diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4100a13f8bee..4f7d1c71553a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,8 +18,8 @@ $ https://github.com/Expensify/App/issues/ Do NOT only link the issue number like this: $ # ---> -$ -PROPOSAL: +$ +PROPOSAL: ### Tests @@ -98,7 +98,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory - [ ] If a new CSS style is added I verified that: - [ ] A similar style doesn't already exist - - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)`) + - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/utils/index.ts) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)`) - [ ] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic. - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. diff --git a/.imgbotconfig b/.imgbotconfig index 43d1b77166cc..45cdf03ec36e 100644 --- a/.imgbotconfig +++ b/.imgbotconfig @@ -1,7 +1,7 @@ { "ignoredFiles": [ - "assets/images/empty-state_background-fade-dark.png", // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499 - "assets/images/empty-state_background-fade-light.png" + "assets/images/themeDependent/empty-state_background-fade-dark.png", // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499 + "assets/images/themeDependent/empty-state_background-fade-light.png" ], "aggressiveCompression": "false" } diff --git a/.storybook/public/index.css b/.storybook/public/index.css index 8ace4b240684..2d2411c083c1 100644 --- a/.storybook/public/index.css +++ b/.storybook/public/index.css @@ -24,5 +24,5 @@ a.sidebar-item[data-selected="true"], a.sidebar-item[data-selected="true"]:focus } .sidebar-container { - background: #07271f; + background: #072419; } diff --git a/.storybook/theme.js b/.storybook/theme.js index 96631764726f..67898fb00943 100644 --- a/.storybook/theme.js +++ b/.storybook/theme.js @@ -7,17 +7,17 @@ export default create({ fontBase: 'ExpensifyNeue-Regular', fontCode: 'monospace', base: 'dark', - appBg: colors.darkHighlightBackground, - colorPrimary: colors.darkDefaultButton, + appBg: colors.productDark200, + colorPrimary: colors.productDark400, colorSecondary: colors.green, - appContentBg: colors.darkAppBackground, - textColor: colors.darkPrimaryText, - barTextColor: colors.darkPrimaryText, + appContentBg: colors.productDark100, + textColor: colors.productDark900, + barTextColor: colors.productDark900, barSelectedColor: colors.green, - barBg: colors.darkAppBackground, - appBorderColor: colors.darkBorders, - inputBg: colors.darkHighlightBackground, - inputBorder: colors.darkBorders, + barBg: colors.productDark100, + appBorderColor: colors.productDark400, + inputBg: colors.productDark200, + inputBorder: colors.productDark400, appBorderRadius: 8, inputBorderRadius: 8, }); diff --git a/android/app/build.gradle b/android/app/build.gradle index 5bf287cc784b..25bb327eb97a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,8 +91,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001040901 - versionName "1.4.9-1" + versionCode 1001041000 + versionName "1.4.10-0" } flavorDimensions "default" diff --git a/assets/images/empty-state_background-fade-dark.png b/assets/images/empty-state_background-fade-dark.png deleted file mode 100644 index 1caf5630bee3..000000000000 Binary files a/assets/images/empty-state_background-fade-dark.png and /dev/null differ diff --git a/assets/images/empty-state_background-fade-light.png b/assets/images/empty-state_background-fade-light.png deleted file mode 100644 index 98456609b502..000000000000 Binary files a/assets/images/empty-state_background-fade-light.png and /dev/null differ diff --git a/assets/images/example-check-image-en.png b/assets/images/example-check-image-en.png deleted file mode 100644 index 903618776cdf..000000000000 Binary files a/assets/images/example-check-image-en.png and /dev/null differ diff --git a/assets/images/example-check-image-es.png b/assets/images/example-check-image-es.png deleted file mode 100644 index de695a43833d..000000000000 Binary files a/assets/images/example-check-image-es.png and /dev/null differ diff --git a/assets/images/home-background--android.svg b/assets/images/home-background--android.svg index 2b72b6ccabe9..29c8affce1cc 100644 --- a/assets/images/home-background--android.svg +++ b/assets/images/home-background--android.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/home-background--mobile.svg b/assets/images/home-background--mobile.svg index 7c4d4d8289b7..d2fa08475c9d 100644 --- a/assets/images/home-background--mobile.svg +++ b/assets/images/home-background--mobile.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/home-fade-gradient--mobile.svg b/assets/images/home-fade-gradient--mobile.svg index 0b24b678a2e6..ad150f3c870c 100644 --- a/assets/images/home-fade-gradient--mobile.svg +++ b/assets/images/home-fade-gradient--mobile.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/home-fade-gradient.svg b/assets/images/home-fade-gradient.svg index bfe04d545364..c446d7b46a42 100644 --- a/assets/images/home-fade-gradient.svg +++ b/assets/images/home-fade-gradient.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/themeDependent/empty-state_background-fade-dark.png b/assets/images/themeDependent/empty-state_background-fade-dark.png new file mode 100644 index 000000000000..59951ef707fb Binary files /dev/null and b/assets/images/themeDependent/empty-state_background-fade-dark.png differ diff --git a/assets/images/themeDependent/empty-state_background-fade-light.png b/assets/images/themeDependent/empty-state_background-fade-light.png new file mode 100644 index 000000000000..200996057b47 Binary files /dev/null and b/assets/images/themeDependent/empty-state_background-fade-light.png differ diff --git a/assets/images/themeDependent/example-check-image-dark-en.png b/assets/images/themeDependent/example-check-image-dark-en.png new file mode 100644 index 000000000000..6b8e84a0faa2 Binary files /dev/null and b/assets/images/themeDependent/example-check-image-dark-en.png differ diff --git a/assets/images/themeDependent/example-check-image-dark-es.png b/assets/images/themeDependent/example-check-image-dark-es.png new file mode 100644 index 000000000000..446678e401b6 Binary files /dev/null and b/assets/images/themeDependent/example-check-image-dark-es.png differ diff --git a/assets/images/themeDependent/example-check-image-light-en.png b/assets/images/themeDependent/example-check-image-light-en.png new file mode 100644 index 000000000000..d7b5f035c625 Binary files /dev/null and b/assets/images/themeDependent/example-check-image-light-en.png differ diff --git a/assets/images/themeDependent/example-check-image-light-es.png b/assets/images/themeDependent/example-check-image-light-es.png new file mode 100644 index 000000000000..2183b522a35b Binary files /dev/null and b/assets/images/themeDependent/example-check-image-light-es.png differ diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 8c74ebfd1686..008b4f45911f 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -211,7 +211,21 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ // This is also why we have to use .website.js for our own web-specific files... // Because desktop also relies on "web-specific" module implementations // This also skips packing web only dependencies to desktop and vice versa - extensions: ['.web.js', platform === 'web' ? '.website.js' : '.desktop.js', '.js', '.jsx', '.web.ts', platform === 'web' ? '.website.ts' : '.desktop.ts', '.ts', '.web.tsx', '.tsx'], + extensions: [ + '.web.js', + ...(platform === 'desktop' ? ['.desktop.js'] : []), + '.website.js', + '.js', + '.jsx', + '.web.ts', + ...(platform === 'desktop' ? ['.desktop.ts'] : []), + '.website.ts', + ...(platform === 'desktop' ? ['.desktop.tsx'] : []), + '.website.tsx', + '.ts', + '.web.tsx', + '.tsx', + ], fallback: { 'process/browser': require.resolve('process/browser'), }, diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index 68088f623f8d..b7cbd87dc4b9 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -46,7 +46,7 @@ - [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory - [ ] If a new CSS style is added I verified that: - [ ] A similar style doesn't already exist - - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG`) + - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/utils/index.ts) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG`) - [ ] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic. - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. diff --git a/docs/_sass/_colors.scss b/docs/_sass/_colors.scss index b34a7d13b7f0..5556c43d87f6 100644 --- a/docs/_sass/_colors.scss +++ b/docs/_sass/_colors.scss @@ -2,8 +2,8 @@ $color-green400: #03D47C; $color-green-icons: #8B9C8F; $color-green-borders: #1A3D32; $color-button-background: #1A3D32; -$color-button-hovered: #2C6755; -$color-green-highlightBG: #07271F; +$color-button-hovered: #2A604F; +$color-green-highlightBG: #072419; $color-green-highlightBG-hover: #06231c; $color-green-appBG: #061B09; $color-green-hover: #00a862; diff --git a/docs/assets/images/settings-old-dot.svg b/docs/assets/images/settings-old-dot.svg index ca5bc04bd0ff..85561a886459 100644 --- a/docs/assets/images/settings-old-dot.svg +++ b/docs/assets/images/settings-old-dot.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e6ff025496f2..61a597638cca 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.9 + 1.4.10 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.9.1 + 1.4.10.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3bf704107dde..189451c9ffdd 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.9 + 1.4.10 CFBundleSignature ???? CFBundleVersion - 1.4.9.1 + 1.4.10.0 diff --git a/package-lock.json b/package-lock.json index e708bbb08809..20ba84846b69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.9-1", + "version": "1.4.10-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.9-1", + "version": "1.4.10-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b014365c3dd5..3491b9ed0293 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.9-1", + "version": "1.4.10-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/patches/react-native-web+0.19.9+005+image-header-support.patch b/patches/react-native-web+0.19.9+005+image-header-support.patch deleted file mode 100644 index 4652e22662f0..000000000000 --- a/patches/react-native-web+0.19.9+005+image-header-support.patch +++ /dev/null @@ -1,200 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js -index 95355d5..19109fc 100644 ---- a/node_modules/react-native-web/dist/exports/Image/index.js -+++ b/node_modules/react-native-web/dist/exports/Image/index.js -@@ -135,7 +135,22 @@ function resolveAssetUri(source) { - } - return uri; - } --var Image = /*#__PURE__*/React.forwardRef((props, ref) => { -+function raiseOnErrorEvent(uri, _ref) { -+ var onError = _ref.onError, -+ onLoadEnd = _ref.onLoadEnd; -+ if (onError) { -+ onError({ -+ nativeEvent: { -+ error: "Failed to load resource " + uri + " (404)" -+ } -+ }); -+ } -+ if (onLoadEnd) onLoadEnd(); -+} -+function hasSourceDiff(a, b) { -+ return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers); -+} -+var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => { - var ariaLabel = props['aria-label'], - blurRadius = props.blurRadius, - defaultSource = props.defaultSource, -@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { - } - }, function error() { - updateState(ERRORED); -- if (onError) { -- onError({ -- nativeEvent: { -- error: "Failed to load resource " + uri + " (404)" -- } -- }); -- } -- if (onLoadEnd) { -- onLoadEnd(); -- } -+ raiseOnErrorEvent(uri, { -+ onError, -+ onLoadEnd -+ }); - }); - } - function abortPendingRequest() { -@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { - suppressHydrationWarning: true - }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); - }); --Image.displayName = 'Image'; -+BaseImage.displayName = 'Image'; -+ -+/** -+ * This component handles specifically loading an image source with headers -+ * default source is never loaded using headers -+ */ -+var ImageWithHeaders = /*#__PURE__*/React.forwardRef((props, ref) => { -+ // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource` -+ var nextSource = props.source; -+ var _React$useState3 = React.useState(''), -+ blobUri = _React$useState3[0], -+ setBlobUri = _React$useState3[1]; -+ var request = React.useRef({ -+ cancel: () => {}, -+ source: { -+ uri: '', -+ headers: {} -+ }, -+ promise: Promise.resolve('') -+ }); -+ var onError = props.onError, -+ onLoadStart = props.onLoadStart, -+ onLoadEnd = props.onLoadEnd; -+ React.useEffect(() => { -+ if (!hasSourceDiff(nextSource, request.current.source)) { -+ return; -+ } -+ -+ // When source changes we want to clean up any old/running requests -+ request.current.cancel(); -+ if (onLoadStart) { -+ onLoadStart(); -+ } -+ -+ // Store a ref for the current load request so we know what's the last loaded source, -+ // and so we can cancel it if a different source is passed through props -+ request.current = ImageLoader.loadWithHeaders(nextSource); -+ request.current.promise.then(uri => setBlobUri(uri)).catch(() => raiseOnErrorEvent(request.current.source.uri, { -+ onError, -+ onLoadEnd -+ })); -+ }, [nextSource, onLoadStart, onError, onLoadEnd]); -+ -+ // Cancel any request on unmount -+ React.useEffect(() => request.current.cancel, []); -+ var propsToPass = _objectSpread(_objectSpread({}, props), {}, { -+ // `onLoadStart` is called from the current component -+ // We skip passing it down to prevent BaseImage raising it a 2nd time -+ onLoadStart: undefined, -+ // Until the current component resolves the request (using headers) -+ // we skip forwarding the source so the base component doesn't attempt -+ // to load the original source -+ source: blobUri ? _objectSpread(_objectSpread({}, nextSource), {}, { -+ uri: blobUri -+ }) : undefined -+ }); -+ return /*#__PURE__*/React.createElement(BaseImage, _extends({ -+ ref: ref -+ }, propsToPass)); -+}); - - // $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet --var ImageWithStatics = Image; -+var ImageWithStatics = /*#__PURE__*/React.forwardRef((props, ref) => { -+ if (props.source && props.source.headers) { -+ return /*#__PURE__*/React.createElement(ImageWithHeaders, _extends({ -+ ref: ref -+ }, props)); -+ } -+ return /*#__PURE__*/React.createElement(BaseImage, _extends({ -+ ref: ref -+ }, props)); -+}); - ImageWithStatics.getSize = function (uri, success, failure) { - ImageLoader.getSize(uri, success, failure); - }; -diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -index bc06a87..e309394 100644 ---- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js -+++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -@@ -76,7 +76,7 @@ var ImageLoader = { - var image = requests["" + requestId]; - if (image) { - var naturalHeight = image.naturalHeight, -- naturalWidth = image.naturalWidth; -+ naturalWidth = image.naturalWidth; - if (naturalHeight && naturalWidth) { - success(naturalWidth, naturalHeight); - complete = true; -@@ -102,11 +102,19 @@ var ImageLoader = { - id += 1; - var image = new window.Image(); - image.onerror = onError; -- image.onload = e => { -+ image.onload = nativeEvent => { - // avoid blocking the main thread -- var onDecode = () => onLoad({ -- nativeEvent: e -- }); -+ var onDecode = () => { -+ // Append `source` to match RN's ImageLoadEvent interface -+ nativeEvent.source = { -+ uri: image.src, -+ width: image.naturalWidth, -+ height: image.naturalHeight -+ }; -+ onLoad({ -+ nativeEvent -+ }); -+ }; - if (typeof image.decode === 'function') { - // Safari currently throws exceptions when decoding svgs. - // We want to catch that error and allow the load handler -@@ -120,6 +128,32 @@ var ImageLoader = { - requests["" + id] = image; - return id; - }, -+ loadWithHeaders(source) { -+ var uri; -+ var abortController = new AbortController(); -+ var request = new Request(source.uri, { -+ headers: source.headers, -+ signal: abortController.signal -+ }); -+ request.headers.append('accept', 'image/*'); -+ var promise = fetch(request).then(response => response.blob()).then(blob => { -+ uri = URL.createObjectURL(blob); -+ return uri; -+ }).catch(error => { -+ if (error.name === 'AbortError') { -+ return ''; -+ } -+ throw error; -+ }); -+ return { -+ promise, -+ source, -+ cancel: () => { -+ abortController.abort(); -+ URL.revokeObjectURL(uri); -+ } -+ }; -+ }, - prefetch(uri) { - return new Promise((resolve, reject) => { - ImageLoader.load(uri, () => { diff --git a/src/CONST.ts b/src/CONST.ts index ddedb550f368..ebba780fd745 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -559,6 +559,7 @@ const CONST = { UPDATE_REPORT_FIELD: 'POLICYCHANGELOG_UPDATE_REPORT_FIELD', UPDATE_TAG: 'POLICYCHANGELOG_UPDATE_TAG', UPDATE_TAG_ENABLED: 'POLICYCHANGELOG_UPDATE_TAG_ENABLED', + UPDATE_TAG_LIST: 'POLICYCHANGELOG_UPDATE_TAG_LIST', UPDATE_TAG_LIST_NAME: 'POLICYCHANGELOG_UPDATE_TAG_LIST_NAME', UPDATE_TAG_NAME: 'POLICYCHANGELOG_UPDATE_TAG_NAME', UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED', diff --git a/src/components/AddressSearch/CurrentLocationButton.js b/src/components/AddressSearch/CurrentLocationButton.js index 6f5148edd436..90d2c15733f1 100644 --- a/src/components/AddressSearch/CurrentLocationButton.js +++ b/src/components/AddressSearch/CurrentLocationButton.js @@ -7,8 +7,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useLocalize from '@hooks/useLocalize'; import getButtonState from '@libs/getButtonState'; import colors from '@styles/colors'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; const propTypes = { @@ -25,14 +24,14 @@ const defaultProps = { }; function CurrentLocationButton({onPress, isDisabled}) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); return ( + () => { ReportActionContextMenu.hideContextMenu(); diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 57b0c6466a7f..4dd0a96c31b9 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -19,8 +19,8 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import useNativeDriver from '@libs/useNativeDriver'; import reportPropTypes from '@pages/reportPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -112,6 +112,7 @@ const defaultProps = { function AttachmentModal(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const onModalHideCallbackRef = useRef(null); const [isModalOpen, setIsModalOpen] = useState(props.defaultOpen); const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index e484abe041b9..6e1ed651ae06 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -15,8 +15,8 @@ import useNetwork from '@hooks/useNetwork'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import compose from '@libs/compose'; import * as TransactionUtils from '@libs/TransactionUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import cursor from '@styles/utilities/cursor'; import variables from '@styles/variables'; @@ -80,6 +80,7 @@ function AttachmentView({ }) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); const [imageError, setImageError] = useState(false); diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index efde2b24992f..07db455968a3 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -5,8 +5,7 @@ import {View} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import viewForwardedRef from '@src/types/utils/viewForwardedRef'; @@ -39,8 +38,8 @@ function BaseAutoCompleteSuggestions( }: AutoCompleteSuggestionsProps, ref: ForwardedRef, ) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); const scrollRef = useRef>(null); /** @@ -49,7 +48,7 @@ function BaseAutoCompleteSuggestions( const renderItem = useCallback( ({item, index}: RenderSuggestionMenuItemProps): ReactElement => ( StyleUtils.getAutoCompleteSuggestionItemStyle(theme, highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)} + style={({hovered}) => StyleUtils.getAutoCompleteSuggestionItemStyle(highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)} hoverDimmingValue={1} onMouseDown={(e) => e.preventDefault()} onPress={() => onSelect(index)} @@ -59,7 +58,7 @@ function BaseAutoCompleteSuggestions( {renderSuggestionMenuItem(item, index)} ), - [highlightedSuggestionIndex, renderSuggestionMenuItem, onSelect, accessibilityLabelExtractor, theme], + [accessibilityLabelExtractor, renderSuggestionMenuItem, StyleUtils, highlightedSuggestionIndex, onSelect], ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 24b846c265a9..3ccbb4efaf5a 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import {View} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; @@ -15,6 +15,7 @@ import type {AutoCompleteSuggestionsProps} from './types'; */ function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { + const StyleUtils = useStyleUtils(); const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); const [{width, left, bottom}, setContainerState] = React.useState({ diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index d394a84bd207..f801cb11e9df 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -3,10 +3,10 @@ import {StyleProp, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import * as ReportUtils from '@libs/ReportUtils'; import {AvatarSource} from '@libs/UserUtils'; -import * as StyleUtils from '@styles/StyleUtils'; -import type {AvatarSizeName} from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; +import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; import {AvatarType} from '@src/types/onyx/OnyxCommon'; import Icon from './Icon'; @@ -60,6 +60,7 @@ function Avatar({ }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [imageError, setImageError] = useState(false); useNetwork({onReconnect: () => setImageError(false)}); @@ -75,8 +76,8 @@ function Avatar({ const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const iconSize = StyleUtils.getAvatarSize(size); - const imageStyle = [StyleUtils.getAvatarStyle(theme, size), imageStyles, styles.noBorderRadius]; - const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(theme, size), styles.bgTransparent, imageStyles] : undefined; + const imageStyle = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; + const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill ?? theme.icon; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.js index a37f228a0d0d..dcb0470c5ee5 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.js +++ b/src/components/AvatarCropModal/AvatarCropModal.js @@ -17,8 +17,8 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import compose from '@libs/compose'; import cropOrRotateImage from '@libs/cropOrRotateImage'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ImageCropView from './ImageCropView'; @@ -63,6 +63,7 @@ const defaultProps = { function AvatarCropModal(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const translateY = useSharedValue(0); diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.js index a50409da64f4..94289b24d6ca 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.js @@ -6,7 +6,7 @@ import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import ControlSelection from '@libs/ControlSelection'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import gestureHandlerPropTypes from './gestureHandlerPropTypes'; @@ -51,6 +51,7 @@ const defaultProps = { function ImageCropView(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize); const originalImageHeight = props.originalImageHeight; diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 9229cb80cf4c..041c180595f1 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -5,8 +5,8 @@ import {ValueOf} from 'type-fest'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -55,6 +55,7 @@ function AvatarWithDisplayName({ }: AvatarWithDisplayNameProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const title = ReportUtils.getReportName(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index ff963589d80f..82212c66db04 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -1,6 +1,6 @@ import React, {useCallback} from 'react'; import {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; @@ -34,12 +34,13 @@ type BadgeProps = { function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const textColorStyles = success || error ? styles.textWhite : undefined; const Wrapper = pressable ? PressableWithoutFeedback : View; const wrapperStyles: (state: PressableStateCallbackType) => StyleProp = useCallback( - ({pressed}) => [styles.badge, styles.ml2, StyleUtils.getBadgeColorStyle(styles, success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC), badgeStyles], - [success, error, environment, badgeStyles, styles], + ({pressed}) => [styles.badge, styles.ml2, StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC), badgeStyles], + [styles.badge, styles.ml2, StyleUtils, success, error, environment, badgeStyles], ); return ( diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 6e5ad8970f1a..cfe817c849c0 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -2,8 +2,7 @@ import React, {memo} from 'react'; import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Hoverable from './Hoverable'; @@ -41,8 +40,8 @@ type BannerProps = { }; function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); return ( @@ -67,7 +66,7 @@ function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRend )} diff --git a/src/components/BaseMiniContextMenuItem.js b/src/components/BaseMiniContextMenuItem.js deleted file mode 100644 index 3252938e4ca5..000000000000 --- a/src/components/BaseMiniContextMenuItem.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import DomUtils from '@libs/DomUtils'; -import getButtonState from '@libs/getButtonState'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; -import variables from '@styles/variables'; -import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; -import Tooltip from './Tooltip/PopoverAnchorTooltip'; - -const propTypes = { - /** - * Text to display when hovering the menu item - */ - tooltipText: PropTypes.string.isRequired, - - /** - * Callback to fire on press - */ - onPress: PropTypes.func.isRequired, - - /** - * The children to display within the menu item - */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /** - * Whether the button should be in the active state - */ - isDelayButtonStateComplete: PropTypes.bool, - - /** - * A ref to forward to the Pressable - */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), -}; - -const defaultProps = { - isDelayButtonStateComplete: true, - innerRef: () => {}, -}; - -/** - * Component that renders a mini context menu item with a - * pressable. Also renders a tooltip when hovering the item. - * @param {Object} props - * @returns {JSX.Element} - */ -function BaseMiniContextMenuItem(props) { - const theme = useTheme(); - const styles = useThemeStyles(); - return ( - - { - if (!ReportActionComposeFocusManager.isFocused() && !ReportActionComposeFocusManager.isEditFocused()) { - DomUtils.getActiveElement().blur(); - return; - } - - // Allow text input blur on right click - if (!e || e.button === 2) { - return; - } - - // Prevent text input blur on left click - e.preventDefault(); - }} - accessibilityLabel={props.tooltipText} - style={({hovered, pressed}) => [ - styles.reportActionContextMenuMiniButton, - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(hovered, pressed, props.isDelayButtonStateComplete)), - props.isDelayButtonStateComplete && styles.cursorDefault, - ]} - > - {(pressableState) => ( - - {_.isFunction(props.children) ? props.children(pressableState) : props.children} - - )} - - - ); -} - -BaseMiniContextMenuItem.propTypes = propTypes; -BaseMiniContextMenuItem.defaultProps = defaultProps; -BaseMiniContextMenuItem.displayName = 'BaseMiniContextMenuItem'; - -const BaseMiniContextMenuItemWithRef = React.forwardRef((props, ref) => ( - -)); - -BaseMiniContextMenuItemWithRef.displayName = 'BaseMiniContextMenuItemWithRef'; - -export default BaseMiniContextMenuItemWithRef; diff --git a/src/components/BaseMiniContextMenuItem.tsx b/src/components/BaseMiniContextMenuItem.tsx new file mode 100644 index 000000000000..082c0e20801a --- /dev/null +++ b/src/components/BaseMiniContextMenuItem.tsx @@ -0,0 +1,85 @@ +import React, {ForwardedRef} from 'react'; +import {PressableStateCallbackType, View} from 'react-native'; +import DomUtils from '@libs/DomUtils'; +import getButtonState from '@libs/getButtonState'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import useStyleUtils from '@styles/useStyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import variables from '@styles/variables'; +import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; +import Tooltip from './Tooltip/PopoverAnchorTooltip'; + +type BaseMiniContextMenuItemProps = { + /** + * Text to display when hovering the menu item + */ + tooltipText: string; + + /** + * Callback to fire on press + */ + onPress: () => void; + + /** + * The children to display within the menu item + */ + children: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode); + + /** + * Whether the button should be in the active state + */ + isDelayButtonStateComplete: boolean; +}; + +/** + * Component that renders a mini context menu item with a + * pressable. Also renders a tooltip when hovering the item. + */ +function BaseMiniContextMenuItem({tooltipText, onPress, children, isDelayButtonStateComplete = true}: BaseMiniContextMenuItemProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + return ( + + { + if (!ReportActionComposeFocusManager.isFocused() && !ReportActionComposeFocusManager.isEditFocused()) { + const activeElement = DomUtils.getActiveElement(); + if (activeElement instanceof HTMLElement) { + activeElement?.blur(); + } + return; + } + + // Allow text input blur on right click + if (!event || event.button === 2) { + return; + } + + // Prevent text input blur on left click + event.preventDefault(); + }} + accessibilityLabel={tooltipText} + style={({hovered, pressed}) => [ + styles.reportActionContextMenuMiniButton, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, isDelayButtonStateComplete)), + isDelayButtonStateComplete && styles.cursorDefault, + ]} + > + {(pressableState) => ( + + {typeof children === 'function' ? children(pressableState) : children} + + )} + + + ); +} + +BaseMiniContextMenuItem.displayName = 'BaseMiniContextMenuItem'; + +export default React.forwardRef(BaseMiniContextMenuItem); diff --git a/src/components/ButtonWithDropdownMenu.js b/src/components/ButtonWithDropdownMenu.js index 15f2e2f4d6de..321cd79f1318 100644 --- a/src/components/ButtonWithDropdownMenu.js +++ b/src/components/ButtonWithDropdownMenu.js @@ -3,8 +3,8 @@ import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Button from './Button'; @@ -74,6 +74,7 @@ const defaultProps = { function ButtonWithDropdownMenu(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [selectedItemIndex, setSelectedItemIndex] = useState(0); const [isMenuVisible, setIsMenuVisible] = useState(false); const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 22577ec2b7f9..5dd3164eadcc 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,7 +1,7 @@ import React, {ForwardedRef, forwardRef, KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -63,6 +63,7 @@ function Checkbox( ) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const handleSpaceKey = (event?: ReactKeyboardEvent) => { if (event?.code !== 'Space') { @@ -98,7 +99,7 @@ function Checkbox( {children ?? ( )} @@ -99,8 +97,8 @@ function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, wrapperStyle={styles.pr9} success={!isThrottledButtonActive} description={description} - descriptionTextStyle={styles.breakAll} - style={getContextMenuItemStyles(styles, windowWidth)} + descriptionTextStyle={styles.breakWord} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} isAnonymousAction={isAnonymousAction} focused={isFocused} interactive={isThrottledButtonActive} diff --git a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx index 685db8031330..6f6daddc51b8 100644 --- a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx +++ b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; import {ValueOf} from 'type-fest'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -26,6 +26,7 @@ type CurrentUserPersonalDetailsSkeletonViewProps = { function CurrentUserPersonalDetailsSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.LARGE, backgroundColor, foregroundColor}: CurrentUserPersonalDetailsSkeletonViewProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize); const avatarPlaceholderRadius = avatarPlaceholderSize / 2; const spaceBetweenAvatarAndHeadline = styles.mb3.marginBottom + styles.mt1.marginTop + (variables.lineHeightXXLarge - variables.fontSizeXLarge) / 2; diff --git a/src/components/DatePicker/CalendarPicker/ArrowIcon.js b/src/components/DatePicker/CalendarPicker/ArrowIcon.js index 1b9c9a06db34..a03e18085706 100644 --- a/src/components/DatePicker/CalendarPicker/ArrowIcon.js +++ b/src/components/DatePicker/CalendarPicker/ArrowIcon.js @@ -3,7 +3,7 @@ import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -22,6 +22,7 @@ const defaultProps = { function ArrowIcon(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index eaa6a8b45b33..a404c4746397 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -8,12 +8,11 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; import ArrowIcon from './ArrowIcon'; import generateMonthMatrix from './generateMonthMatrix'; @@ -34,7 +33,7 @@ const propTypes = { ...withLocalizePropTypes, ...withThemeStylesPropTypes, - ...withThemePropTypes, + ...withStyleUtilsPropTypes, }; const defaultProps = { @@ -238,7 +237,7 @@ class CalendarPicker extends React.PureComponent { style={[ this.props.themeStyles.calendarDayContainer, isSelected ? this.props.themeStyles.calendarDayContainerSelected : {}, - !isDisabled ? StyleUtils.getButtonBackgroundColorStyle(this.props.theme, getButtonState(hovered, pressed)) : {}, + !isDisabled ? this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed)) : {}, ]} > {day} @@ -264,4 +263,4 @@ class CalendarPicker extends React.PureComponent { CalendarPicker.propTypes = propTypes; CalendarPicker.defaultProps = defaultProps; -export default compose(withLocalize, withTheme, withThemeStyles)(CalendarPicker); +export default compose(withLocalize, withThemeStyles, withStyleUtils)(CalendarPicker); diff --git a/src/components/DistanceMapView/index.android.js b/src/components/DistanceMapView/index.android.js index 848167de653d..532d42ac0be5 100644 --- a/src/components/DistanceMapView/index.android.js +++ b/src/components/DistanceMapView/index.android.js @@ -6,12 +6,13 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MapView from '@components/MapView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as distanceMapViewPropTypes from './distanceMapViewPropTypes'; function DistanceMapView(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isMapReady, setIsMapReady] = useState(false); const {isOffline} = useNetwork(); const {translate} = useLocalize(); diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index b90093e20fc3..6a7d78768ed7 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -3,8 +3,8 @@ import React from 'react'; import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import fileDownload from '@libs/fileDownload'; import * as Localize from '@libs/Localize'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Icon from './Icon'; @@ -45,6 +45,7 @@ function isReceiptError(message: string | ReceiptError): message is ReceiptError function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndicatorMessageProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); if (Object.keys(messages).length === 0) { return null; @@ -92,7 +93,7 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica {message} diff --git a/src/components/EReceipt.js b/src/components/EReceipt.js index 85c753c7ccb3..f5e5b7f2f6b3 100644 --- a/src/components/EReceipt.js +++ b/src/components/EReceipt.js @@ -6,7 +6,7 @@ import useLocalize from '@hooks/useLocalize'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -31,6 +31,7 @@ const defaultProps = { function EReceipt({transaction, transactionID}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); // Get receipt colorway, or default to Yellow. diff --git a/src/components/EReceiptThumbnail.js b/src/components/EReceiptThumbnail.js index f54e246b8b1e..5eb35538af80 100644 --- a/src/components/EReceiptThumbnail.js +++ b/src/components/EReceiptThumbnail.js @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -37,12 +37,9 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function getBackgroundImage(transaction) { - return backgroundImages[StyleUtils.getEReceiptColorCode(transaction)]; -} - function EReceiptThumbnail({transaction}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); // Get receipt colorway, or default to Yellow. const {backgroundColor: primaryColor, color: secondaryColor} = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)); @@ -75,6 +72,8 @@ function EReceiptThumbnail({transaction}) { receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; } + const getBackgroundImage = useMemo((trans) => backgroundImages[StyleUtils.getEReceiptColorCode(trans)], [StyleUtils]); + return ( setIsHighlighted(true)} onHoverOut={() => setIsHighlighted(false)} - style={({pressed}) => [ - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(false, pressed)), - styles.categoryShortcutButton, - isHighlighted && styles.emojiItemHighlighted, - ]} + style={({pressed}) => [StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), styles.categoryShortcutButton, isHighlighted && styles.emojiItemHighlighted]} accessibilityLabel={`emojiPicker.headers.${props.code}`} role={CONST.ACCESSIBILITY_ROLE.BUTTON} > diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index 9fc1224f96c0..96d7ee88b816 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -6,7 +6,7 @@ import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import withViewportOffsetTop from '@components/withViewportOffsetTop'; import useWindowDimensions from '@hooks/useWindowDimensions'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import EmojiPickerMenu from './EmojiPickerMenu'; @@ -22,6 +22,7 @@ const propTypes = { const EmojiPicker = forwardRef((props, ref) => { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isEmojiPickerVisible, setIsEmojiPickerVisible] = useState(false); const [emojiPopoverAnchorPosition, setEmojiPopoverAnchorPosition] = useState({ horizontal: 0, diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index 2926d6346b1b..165646d4795d 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -6,8 +6,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; @@ -31,8 +30,8 @@ const defaultProps = { }; function EmojiPickerButton(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); @@ -41,7 +40,7 @@ function EmojiPickerButton(props) { [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(hovered, pressed))]} + style={({hovered, pressed}) => [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed))]} disabled={props.isDisabled} onPress={() => { if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { @@ -56,7 +55,7 @@ function EmojiPickerButton(props) { {({hovered, pressed}) => ( )} diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js index 6fd24adf04aa..02a5954cb705 100644 --- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js +++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js @@ -8,8 +8,7 @@ import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import CONST from '@src/CONST'; @@ -26,8 +25,8 @@ const defaultProps = { }; function EmojiPickerButtonDropdown(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); @@ -66,7 +65,7 @@ function EmojiPickerButtonDropdown(props) { diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 2c77b393c2b9..95db6eb41167 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -18,7 +18,7 @@ import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; @@ -52,6 +52,7 @@ function EmojiPickerMenu(props) { const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, translate} = props; const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); @@ -318,13 +319,13 @@ function EmojiPickerMenu(props) { // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input // is not focused, so that the navigation and tab cycling can be done using the keyboard without // interfering with the input behaviour. - if (!ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !searchInputRef.current.isFocused())) { setIsUsingKeyboardMovement(true); return; } // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (searchInputRef.current && !searchInputRef.current.isFocused()) { + if (searchInputRef.current && !searchInputRef.current.isFocused() && ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { searchInputRef.current.focus(); } }, diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 772c32ff4a88..f1560c07b397 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -15,7 +15,7 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; @@ -43,6 +43,7 @@ const defaultProps = { function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const emojiList = useAnimatedRef(); // eslint-disable-next-line react-hooks/exhaustive-deps const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]); diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js index f674d3c4fa0e..ae2cdf46dfc0 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js @@ -2,11 +2,10 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import * as Browser from '@libs/Browser'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; const propTypes = { @@ -35,7 +34,7 @@ const propTypes = { isHighlighted: PropTypes.bool, ...withThemeStylesPropTypes, - ...withThemePropTypes, + ...withStyleUtilsPropTypes, }; class EmojiPickerMenuItem extends PureComponent { @@ -100,7 +99,7 @@ class EmojiPickerMenuItem extends PureComponent { style={({pressed}) => [ this.props.isFocused ? this.props.themeStyles.emojiItemKeyboardHighlighted : {}, this.state.isHovered || this.props.isHighlighted ? this.props.themeStyles.emojiItemHighlighted : {}, - Browser.isMobile() && StyleUtils.getButtonBackgroundColorStyle(this.props.theme, getButtonState(false, pressed)), + Browser.isMobile() && this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), this.props.themeStyles.emojiItem, ]} accessibilityLabel={this.props.emoji} @@ -124,8 +123,8 @@ EmojiPickerMenuItem.defaultProps = { // Significantly speeds up re-renders of the EmojiPickerMenu's FlatList // by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. -export default withTheme( - withThemeStyles( +export default withThemeStyles( + withStyleUtils( React.memo( EmojiPickerMenuItem, (prevProps, nextProps) => prevProps.isFocused === nextProps.isFocused && prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji, diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js index ca24dde6b861..7934cc0f03d4 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js @@ -2,10 +2,9 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; const propTypes = { @@ -37,7 +36,7 @@ const propTypes = { isUsingKeyboardMovement: PropTypes.bool, ...withThemeStylesPropTypes, - ...withThemePropTypes, + ...withStyleUtilsPropTypes, }; class EmojiPickerMenuItem extends PureComponent { @@ -75,7 +74,7 @@ class EmojiPickerMenuItem extends PureComponent { onBlur={this.props.onBlur} ref={(ref) => (this.ref = ref)} style={({pressed}) => [ - StyleUtils.getButtonBackgroundColorStyle(this.props.theme, getButtonState(false, pressed)), + this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), this.props.isHighlighted && this.props.isUsingKeyboardMovement ? this.props.themeStyles.emojiItemKeyboardHighlighted : {}, this.props.isHighlighted && !this.props.isUsingKeyboardMovement ? this.props.themeStyles.emojiItemHighlighted : {}, this.props.themeStyles.emojiItem, @@ -102,8 +101,8 @@ EmojiPickerMenuItem.defaultProps = { // Significantly speeds up re-renders of the EmojiPickerMenu's FlatList // by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. -export default withTheme( - withThemeStyles( +export default withThemeStyles( + withStyleUtils( React.memo( EmojiPickerMenuItem, (prevProps, nextProps) => diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 6917d3dec185..01f840677e5e 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -3,8 +3,7 @@ import {View} from 'react-native'; import type {SimpleEmoji} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; import Text from './Text'; @@ -43,8 +42,8 @@ type EmojiSuggestionsProps = { const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`; function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); /** * Render an emoji suggestion menu item component. */ @@ -63,7 +62,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr {styledTextArray.map(({text, isColored}) => ( {text} @@ -73,7 +72,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr ); }, - [styles, theme, prefix, preferredSkinToneIndex], + [prefix, styles.autoCompleteSuggestionContainer, styles.emojiSuggestionsEmoji, styles.emojiSuggestionsText, preferredSkinToneIndex, StyleUtils], ); return ( diff --git a/src/components/ExpensifyWordmark.tsx b/src/components/ExpensifyWordmark.tsx index 1402b48df0d9..49559d1cc6d5 100644 --- a/src/components/ExpensifyWordmark.tsx +++ b/src/components/ExpensifyWordmark.tsx @@ -5,8 +5,8 @@ import DevLogo from '@assets/images/expensify-logo--dev.svg'; import StagingLogo from '@assets/images/expensify-logo--staging.svg'; import ProductionLogo from '@assets/images/expensify-wordmark.svg'; import useEnvironment from '@hooks/useEnvironment'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -28,6 +28,7 @@ const logoComponents = { function ExpensifyWordmark({isSmallScreenWidth, style}: ExpensifyWordmarkProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {environment} = useEnvironment(); // PascalCase is required for React components, so capitalize the const here const LogoComponent = logoComponents[environment]; diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index c49f69c336eb..791eb150f8c9 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -2,12 +2,12 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import {Animated, Easing, View} from 'react-native'; import compose from '@libs/compose'; -import * as StyleUtils from '@styles/StyleUtils'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import withStyleUtils, {withStyleUtilsPropTypes} from './withStyleUtils'; import withTheme, {withThemePropTypes} from './withTheme'; import withThemeStyles, {withThemeStylesPropTypes} from './withThemeStyles'; @@ -28,8 +28,9 @@ const propTypes = { buttonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), ...withLocalizePropTypes, - ...withThemeStylesPropTypes, ...withThemePropTypes, + ...withThemeStylesPropTypes, + ...withStyleUtilsPropTypes, }; const defaultProps = { @@ -100,7 +101,7 @@ class FloatingActionButton extends PureComponent { this.props.onPress(e); }} onLongPress={() => {}} - style={[this.props.themeStyles.floatingActionButton, StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} + style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} > FloatingActionButtonWithLocalizeWithRef.displayName = 'FloatingActionButtonWithLocalizeWithRef'; -export default compose(withThemeStyles, withTheme)(FloatingActionButtonWithLocalizeWithRef); +export default compose(withThemeStyles, withTheme, withStyleUtils)(FloatingActionButtonWithLocalizeWithRef); diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js index 1b59219f38be..457a9dce66d9 100644 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js @@ -1,7 +1,7 @@ import React from 'react'; import {Animated} from 'react-native'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; @@ -11,6 +11,7 @@ const propTypes = { function GrowlNotificationContainer(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const insets = useSafeAreaInsets; return ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js index 8cc062d754bc..9d101b8a5190 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js @@ -3,10 +3,11 @@ import {splitBoxModelStyle} from 'react-native-render-html'; import _ from 'underscore'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import InlineCodeBlock from '@components/InlineCodeBlock'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import htmlRendererPropTypes from './htmlRendererPropTypes'; function CodeRenderer(props) { + const StyleUtils = useStyleUtils(); // We split wrapper and inner styles // "boxModelStyle" corresponds to border, margin, padding and backgroundColor const {boxModelStyle, otherStyle: textStyle} = splitBoxModelStyle(props.style); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js index 65c287b8e86b..82769598d84a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js @@ -2,18 +2,17 @@ import React from 'react'; import {TNodeChildrenRenderer} from 'react-native-render-html'; import _ from 'underscore'; import Text from '@components/Text'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import htmlRendererPropTypes from './htmlRendererPropTypes'; function MentionHereRenderer(props) { - const theme = useTheme(); + const StyleUtils = useStyleUtils(); return ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 0027a557ab02..fbdacb6b47b0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -13,8 +13,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -28,8 +27,8 @@ const propTypes = { }; function MentionUserRenderer(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); const htmlAttribAccountID = lodashGet(props.tnode.attributes, 'accountid'); @@ -77,7 +76,7 @@ function MentionUserRenderer(props) { }} > func, shouldNavigateToTopMostReport = false, }) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState(); const {translate} = useLocalize(); const {isKeyboardShown} = useKeyboardState(); @@ -133,7 +132,7 @@ function HeaderWithBackButton({ > diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 25bd0a083be0..80abe1872c12 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,8 +1,8 @@ import React, {PureComponent} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; -import withTheme, {ThemeProps} from '@components/withTheme'; -import withThemeStyles, {type ThemeStylesProps} from '@components/withThemeStyles'; -import * as StyleUtils from '@styles/StyleUtils'; +import withStyleUtils, {WithStyleUtilsProps} from '@components/withStyleUtils'; +import withTheme, {WithThemeProps} from '@components/withTheme'; +import withThemeStyles, {type WithThemeStylesProps} from '@components/withThemeStyles'; import variables from '@styles/variables'; import IconWrapperStyles from './IconWrapperStyles'; @@ -41,8 +41,9 @@ type IconProps = { /** Additional styles to add to the Icon */ additionalStyles?: StyleProp; -} & ThemeStylesProps & - ThemeProps; +} & WithThemeStylesProps & + WithThemeProps & + WithStyleUtilsProps; // We must use a class component to create an animatable component with the Animated API // eslint-disable-next-line react/prefer-stateless-function @@ -62,14 +63,14 @@ class Icon extends PureComponent { render() { const width = this.props.small ? variables.iconSizeSmall : this.props.width; const height = this.props.small ? variables.iconSizeSmall : this.props.height; - const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, this.props.themeStyles.pAbsolute, this.props.additionalStyles]; + const iconStyles = [this.props.StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, this.props.themeStyles.pAbsolute, this.props.additionalStyles]; const fill = this.props.fill ?? this.props.theme.icon; if (this.props.inline) { return ( { } } -export default withTheme(withThemeStyles(Icon)); +export default withTheme(withThemeStyles(withStyleUtils(Icon))); diff --git a/src/components/Image/BaseImage.js b/src/components/Image/BaseImage.js deleted file mode 100644 index cd2326900c6c..000000000000 --- a/src/components/Image/BaseImage.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, {useCallback} from 'react'; -import {Image as RNImage} from 'react-native'; -import {defaultProps, imagePropTypes} from './imagePropTypes'; - -function BaseImage({onLoad, ...props}) { - const imageLoadedSuccessfully = useCallback( - ({nativeEvent}) => { - // We override `onLoad`, so both web and native have the same signature - const {width, height} = nativeEvent.source; - onLoad({nativeEvent: {width, height}}); - }, - [onLoad], - ); - - return ( - - ); -} - -BaseImage.propTypes = imagePropTypes; -BaseImage.defaultProps = defaultProps; -BaseImage.displayName = 'BaseImage'; - -export default BaseImage; diff --git a/src/components/Image/BaseImage.native.js b/src/components/Image/BaseImage.native.js deleted file mode 100644 index a621947267a1..000000000000 --- a/src/components/Image/BaseImage.native.js +++ /dev/null @@ -1,3 +0,0 @@ -import RNFastImage from 'react-native-fast-image'; - -export default RNFastImage; diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 8cee1cf95e14..ef1a69e19c12 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,35 +1,51 @@ import lodashGet from 'lodash/get'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; +import {Image as RNImage} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import CONST from '@src/CONST'; +import _ from 'underscore'; import ONYXKEYS from '@src/ONYXKEYS'; -import BaseImage from './BaseImage'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; -function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) { - // Update the source to include the auth token if required +function Image(props) { + const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; + /** + * Check if the image source is a URL - if so the `encryptedAuthToken` is appended + * to the source. + */ const source = useMemo(() => { - if (typeof lodashGet(propsSource, 'uri') === 'number') { - return propsSource.uri; + if (isAuthTokenRequired) { + // There is currently a `react-native-web` bug preventing the authToken being passed + // in the headers of the image request so the authToken is added as a query param. + // On native the authToken IS passed in the image request headers + const authToken = lodashGet(session, 'encryptedAuthToken', null); + return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; } - if (typeof propsSource !== 'number' && isAuthTokenRequired) { - const authToken = lodashGet(session, 'encryptedAuthToken'); - return { - ...propsSource, - headers: { - [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken, - }, - }; - } - return propsSource; // The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034. // eslint-disable-next-line react-hooks/exhaustive-deps }, [propsSource, isAuthTokenRequired]); + /** + * The natural image dimensions are retrieved using the updated source + * and as a result the `onLoad` event needs to be manually invoked to return these dimensions + */ + useEffect(() => { + // If an onLoad callback was specified then manually call it and pass + // the natural image dimensions to match the native API + if (onLoad == null) { + return; + } + RNImage.getSize(source.uri, (width, height) => { + onLoad({nativeEvent: {width, height}}); + }); + }, [onLoad, source]); + + // Omit the props which the underlying RNImage won't use + const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']); + return ( - { + const {width, height} = evt.nativeEvent; + dimensionsCache.set(source.uri, {width, height}); + if (props.onLoad) { + props.onLoad(evt); + } + }} + /> + ); +} + +Image.propTypes = imagePropTypes; +Image.defaultProps = defaultProps; +Image.displayName = 'Image'; +const ImageWithOnyx = withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(Image); +ImageWithOnyx.resizeMode = RESIZE_MODES; +ImageWithOnyx.resolveDimensions = resolveDimensions; + +export default ImageWithOnyx; diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js index fd607a0e81a9..1fd81277b545 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.js @@ -5,7 +5,7 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -33,6 +33,7 @@ const defaultProps = { function ImageView({isAuthTokenRequired, url, fileName, onError}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); const [containerHeight, setContainerHeight] = useState(0); const [containerWidth, setContainerWidth] = useState(0); @@ -257,7 +258,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { = 1 ? styles.pRelative : styles.pAbsolute), ...styles.flex1, }} diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 1765c85cdd48..053f201ac109 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -24,9 +24,8 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import * as ReportUtils from '@libs/ReportUtils'; import * as ContextMenuActions from '@pages/home/report/ContextMenu/ContextMenuActions'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import * as optionRowStyles from '@styles/optionRowStyles'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -66,6 +65,7 @@ const defaultProps = { function OptionRowLHN(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const popoverAnchor = useRef(null); const isFocusedRef = useRef(true); const {isSmallScreenWidth} = useWindowDimensions(); @@ -103,7 +103,7 @@ function OptionRowLHN(props) { props.style, ); const contentContainerStyles = - props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, optionRowStyles.compactContentContainerStyles(styles)] : [styles.flex1]; + props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten( props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] @@ -158,6 +158,8 @@ function OptionRowLHN(props) { optionItem.type === CONST.REPORT.TYPE.CHAT && _.isEmpty(optionItem.chatType) && !optionItem.isThread && lodashGet(optionItem, 'displayNamesWithTooltips.length', 0) > 2; const fullTitle = isGroupChat ? getGroupChatName(ReportUtils.getReport(optionItem.reportID)) : optionItem.text; + const subscriptAvatarBorderColor = props.isFocused ? focusedBackgroundColor : theme.sidebar; + return ( {isPermissionDenied ? ( - {`${translate('location.permissionDenied')} ${translate('location.please')}`} + {`${translate('location.permissionDenied')} ${translate('location.please')}`} {` ${translate('location.allowPermission')} `} - {translate('location.tryAgain')} + {translate('location.tryAgain')} ) : ( {translate('location.notFound')} diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index c40962c8e631..91430602a115 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -6,7 +6,7 @@ import _ from 'underscore'; import useNetwork from '@hooks/useNetwork'; import * as Browser from '@libs/Browser'; import * as ValidationUtils from '@libs/ValidationUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; @@ -108,6 +108,7 @@ const getInputPlaceholderSlots = (length) => Array.from(Array(length).keys()); function MagicCodeInput(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const inputRefs = useRef(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); const [focusedIndex, setFocusedIndex] = useState(0); @@ -407,7 +408,7 @@ function MagicCodeInput(props) { ( const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index ae4b566f98ee..b42540bacb13 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -1,8 +1,8 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import getStyledTextArray from '@libs/GetStyledTextArray'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import {Icon} from '@src/types/onyx/OnyxCommon'; @@ -54,6 +54,7 @@ const keyExtractor = (item: Mention) => item.alternateText; function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainer = () => {}}: MentionSuggestionsProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); /** * Render a suggestion menu item component. */ @@ -83,7 +84,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe {text} @@ -99,7 +100,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe {text} @@ -109,7 +110,19 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe ); }, - [styles, theme, prefix], + [ + prefix, + styles.autoCompleteSuggestionContainer, + styles.ph2, + styles.mentionSuggestionsAvatarContainer, + styles.mentionSuggestionsText, + styles.flexShrink1, + styles.flex1, + styles.mentionSuggestionsDisplayName, + styles.mentionSuggestionsHandle, + theme.success, + StyleUtils, + ], ); return ( diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index a551d4a9205a..0e07fcd22b4c 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -7,8 +7,8 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -88,6 +88,7 @@ const defaultProps = { const MenuItem = React.forwardRef((props, ref) => { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const style = StyleUtils.combineStyles(props.style, styles.popoverMenuItem); const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); @@ -181,7 +182,7 @@ const MenuItem = React.forwardRef((props, ref) => { style={({pressed}) => [ style, !props.interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), (isHovered || pressed) && props.hoverAndPressStyle, ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, @@ -227,7 +228,6 @@ const MenuItem = React.forwardRef((props, ref) => { fill={ props.iconFill || StyleUtils.getIconFillColor( - theme, getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true, ) @@ -262,11 +262,7 @@ const MenuItem = React.forwardRef((props, ref) => { height={props.iconHeight} fill={ props.secondaryIconFill || - StyleUtils.getIconFillColor( - theme, - getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), - true, - ) + StyleUtils.getIconFillColor(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true) } /> @@ -382,7 +378,7 @@ const MenuItem = React.forwardRef((props, ref) => { )} diff --git a/src/components/MessagesRow.js b/src/components/MessagesRow.tsx similarity index 60% rename from src/components/MessagesRow.js rename to src/components/MessagesRow.tsx index e4d6240ba0fd..02b78942dfcf 100644 --- a/src/components/MessagesRow.js +++ b/src/components/MessagesRow.tsx @@ -1,55 +1,45 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {StyleProp, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; +import * as Localize from '@libs/Localize'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DotIndicatorMessage from './DotIndicatorMessage'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip'; -const propTypes = { +type MessagesRowProps = { /** The messages to display */ - messages: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.oneOfType([PropTypes.string, PropTypes.object]), PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - ), + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ - type: PropTypes.oneOf(['error', 'success']).isRequired, + type: 'error' | 'success'; /** A function to run when the X button next to the message is clicked */ - onClose: PropTypes.func, + onClose?: () => void; /** Additional style object for the container */ - containerStyles: stylePropTypes, + containerStyles?: StyleProp; /** Whether we can dismiss the messages */ - canDismiss: PropTypes.bool, + canDismiss?: boolean; }; -const defaultProps = { - messages: {}, - onClose: () => {}, - containerStyles: [], - canDismiss: true, -}; - -function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) { +function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles, canDismiss = true}: MessagesRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - if (_.isEmpty(messages)) { + + if (isEmptyObject(messages)) { return null; } return ( - + @@ -69,8 +59,6 @@ function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) { ); } -MessagesRow.propTypes = propTypes; -MessagesRow.defaultProps = defaultProps; MessagesRow.displayName = 'MessagesRow'; export default MessagesRow; diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index f5c498b24893..1ea284b55280 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -7,9 +7,8 @@ import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import useNativeDriver from '@libs/useNativeDriver'; -import getModalStyles from '@styles/getModalStyles'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; @@ -44,6 +43,7 @@ function BaseModal( ) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); const safeAreaInsets = useSafeAreaInsets(); @@ -133,9 +133,7 @@ function BaseModal( hideBackdrop, } = useMemo( () => - getModalStyles( - theme, - styles, + StyleUtils.getModalStyles( type, { windowWidth, @@ -146,7 +144,7 @@ function BaseModal( innerContainerStyle, outerStyle, ), - [innerContainerStyle, isSmallScreenWidth, outerStyle, popoverAnchorPosition, theme, type, windowHeight, windowWidth, styles], + [StyleUtils, type, windowWidth, windowHeight, isSmallScreenWidth, popoverAnchorPosition, innerContainerStyle, outerStyle], ); const { diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index bc683b6f6311..bfc899f9c278 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,14 +1,15 @@ import React, {useState} from 'react'; import withWindowDimensions from '@components/withWindowDimensions'; import StatusBar from '@libs/StatusBar'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import CONST from '@src/CONST'; import BaseModal from './BaseModal'; import BaseModalProps from './types'; function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = () => {}, children, ...rest}: BaseModalProps) { const theme = useTheme(); + const StyleUtils = useStyleUtils(); const [previousStatusBarColor, setPreviousStatusBarColor] = useState(); const setStatusBarColor = (color = theme.appBG) => { @@ -32,7 +33,7 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( if (statusBarColor) { setPreviousStatusBarColor(statusBarColor); // If it is a full screen modal then match it with appBG, otherwise we use the backdrop color - setStatusBarColor(isFullScreenModal ? theme.appBG : StyleUtils.getThemeBackgroundColor(theme, statusBarColor)); + setStatusBarColor(isFullScreenModal ? theme.appBG : StyleUtils.getThemeBackgroundColor(statusBarColor)); } onModalShow?.(); diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index efff279324ac..a7b22a663e08 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -2,8 +2,8 @@ import React, {memo, useMemo} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import {ValueOf} from 'type-fest'; import {AvatarSource} from '@libs/UserUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -80,6 +80,7 @@ function MultipleAvatars({ }: MultipleAvatarsProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const avatarSizeToStylesMap: AvatarSizeToStylesMap = useMemo( () => ({ @@ -101,7 +102,7 @@ function MultipleAvatars({ const secondAvatarStyle = secondAvatarStyleProp ?? [StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)]; - let avatarContainerStyles = StyleUtils.getContainerStyles(styles, size, isInReportAction); + let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size, avatarSizeToStylesMap]); const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => icon.name) : ['']), [shouldShowTooltip, icons]); @@ -161,7 +162,7 @@ function MultipleAvatars({ ); } - const oneAvatarSize = StyleUtils.getAvatarStyle(theme, size); + const oneAvatarSize = StyleUtils.getAvatarStyle(size); const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; const overlapSize = oneAvatarSize.width / 3; diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js deleted file mode 100644 index 7a2dfbd2b6da..000000000000 --- a/src/components/OfflineWithFeedback.js +++ /dev/null @@ -1,143 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import useNetwork from '@hooks/useNetwork'; -import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; -import useThemeStyles from '@styles/useThemeStyles'; -import CONST from '@src/CONST'; -import MessagesRow from './MessagesRow'; - -/** - * This component should be used when we are using the offline pattern B (offline with feedback). - * You should enclose any element that should have feedback that the action was taken offline and it will take - * care of adding the appropriate styles for pending actions and displaying the dismissible error. - */ - -const propTypes = { - /** The type of action that's pending */ - pendingAction: PropTypes.oneOf(['add', 'update', 'delete']), - - /** Determine whether to hide the component's children if deletion is pending */ - shouldHideOnDelete: PropTypes.bool, - - /** The errors to display */ - // eslint-disable-next-line react/forbid-prop-types - errors: PropTypes.object, - - /** Whether we should show the error messages */ - shouldShowErrorMessages: PropTypes.bool, - - /** Whether we should disable opacity */ - shouldDisableOpacity: PropTypes.bool, - - /** A function to run when the X button next to the error is clicked */ - onClose: PropTypes.func, - - /** The content that needs offline feedback */ - children: PropTypes.node.isRequired, - - /** Additional styles to add after local styles. Applied to the parent container */ - style: stylePropTypes, - - /** Additional styles to add after local styles. Applied to the children wrapper container */ - contentContainerStyle: stylePropTypes, - - /** Additional style object for the error row */ - errorRowStyles: stylePropTypes, - - /** Whether applying strikethrough to the children should be disabled */ - shouldDisableStrikeThrough: PropTypes.bool, - - /** Whether to apply needsOffscreenAlphaCompositing prop to the children */ - needsOffscreenAlphaCompositing: PropTypes.bool, - - /** Whether we can dismiss the error message */ - canDismissError: PropTypes.bool, -}; - -const defaultProps = { - pendingAction: null, - shouldHideOnDelete: true, - errors: null, - shouldShowErrorMessages: true, - shouldDisableOpacity: false, - onClose: () => {}, - style: [], - contentContainerStyle: [], - errorRowStyles: [], - shouldDisableStrikeThrough: false, - needsOffscreenAlphaCompositing: false, - canDismissError: true, -}; - -/** - * This method applies the strikethrough to all the children passed recursively - * @param {Array} children - * @param {Object} styles - * @return {Array} - */ -function applyStrikeThrough(children, styles) { - return React.Children.map(children, (child) => { - if (!React.isValidElement(child)) { - return child; - } - const props = {style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted, styles.userSelectNone)}; - if (child.props.children) { - props.children = applyStrikeThrough(child.props.children, styles); - } - return React.cloneElement(child, props); - }); -} - -function OfflineWithFeedback(props) { - const styles = useThemeStyles(); - const {isOffline} = useNetwork(); - - const hasErrors = !_.isEmpty(props.errors); - - // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = _.omit(props.errors, (e) => e === null); - const hasErrorMessages = !_.isEmpty(errorMessages); - const isOfflinePendingAction = isOffline && props.pendingAction; - const isUpdateOrDeleteError = hasErrors && (props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - const isAddError = hasErrors && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; - const needsOpacity = !props.shouldDisableOpacity && ((isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError); - const needsStrikeThrough = !props.shouldDisableStrikeThrough && isOffline && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - const hideChildren = props.shouldHideOnDelete && !isOffline && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !hasErrors; - let children = props.children; - - // Apply strikethrough to children if needed, but skip it if we are not going to render them - if (needsStrikeThrough && !hideChildren) { - children = applyStrikeThrough(children, styles); - } - return ( - - {!hideChildren && ( - - {children} - - )} - {props.shouldShowErrorMessages && hasErrorMessages && ( - - )} - - ); -} - -OfflineWithFeedback.propTypes = propTypes; -OfflineWithFeedback.defaultProps = defaultProps; -OfflineWithFeedback.displayName = 'OfflineWithFeedback'; - -export default OfflineWithFeedback; diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx new file mode 100644 index 000000000000..78c93c250f84 --- /dev/null +++ b/src/components/OfflineWithFeedback.tsx @@ -0,0 +1,147 @@ +import React, {useCallback} from 'react'; +import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; +import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; +import useStyleUtils from '@styles/useStyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; +import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import MessagesRow from './MessagesRow'; + +/** + * This component should be used when we are using the offline pattern B (offline with feedback). + * You should enclose any element that should have feedback that the action was taken offline and it will take + * care of adding the appropriate styles for pending actions and displaying the dismissible error. + */ + +type OfflineWithFeedbackProps = ChildrenProps & { + /** The type of action that's pending */ + pendingAction: OnyxCommon.PendingAction; + + /** Determine whether to hide the component's children if deletion is pending */ + shouldHideOnDelete?: boolean; + + /** The errors to display */ + errors?: OnyxCommon.Errors; + + /** Whether we should show the error messages */ + shouldShowErrorMessages?: boolean; + + /** Whether we should disable opacity */ + shouldDisableOpacity?: boolean; + + /** A function to run when the X button next to the error is clicked */ + onClose?: () => void; + + /** Additional styles to add after local styles. Applied to the parent container */ + style?: StyleProp; + + /** Additional styles to add after local styles. Applied to the children wrapper container */ + contentContainerStyle?: StyleProp; + + /** Additional style object for the error row */ + errorRowStyles?: StyleProp; + + /** Whether applying strikethrough to the children should be disabled */ + shouldDisableStrikeThrough?: boolean; + + /** Whether to apply needsOffscreenAlphaCompositing prop to the children */ + needsOffscreenAlphaCompositing?: boolean; + + /** Whether we can dismiss the error message */ + canDismissError?: boolean; +}; + +type StrikethroughProps = Partial & {style: Array}; + +function omitBy(obj: Record | undefined, predicate: (value: T) => boolean) { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars + return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, value]) => !predicate(value))); +} + +function OfflineWithFeedback({ + pendingAction, + canDismissError = true, + contentContainerStyle, + errorRowStyles, + errors, + needsOffscreenAlphaCompositing = false, + onClose = () => {}, + shouldDisableOpacity = false, + shouldDisableStrikeThrough = false, + shouldHideOnDelete = true, + shouldShowErrorMessages = true, + style, + ...rest +}: OfflineWithFeedbackProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {isOffline} = useNetwork(); + + const hasErrors = isNotEmptyObject(errors ?? {}); + // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. + const errorMessages = omitBy(errors, (e) => e === null); + const hasErrorMessages = isNotEmptyObject(errorMessages); + const isOfflinePendingAction = !!isOffline && !!pendingAction; + const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + const needsOpacity = !shouldDisableOpacity && ((isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError); + const needsStrikeThrough = !shouldDisableStrikeThrough && isOffline && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const hideChildren = shouldHideOnDelete && !isOffline && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !hasErrors; + let children = rest.children; + + /** + * This method applies the strikethrough to all the children passed recursively + */ + const applyStrikeThrough = useCallback( + (childrenProp: React.ReactNode): React.ReactNode => + React.Children.map(childrenProp, (child) => { + if (!React.isValidElement(child)) { + return child; + } + + const props: StrikethroughProps = { + style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted, styles.userSelectNone), + }; + + if (child.props.children) { + props.children = applyStrikeThrough(child.props.children); + } + + return React.cloneElement(child, props); + }), + [StyleUtils, styles], + ); + + // Apply strikethrough to children if needed, but skip it if we are not going to render them + if (needsStrikeThrough && !hideChildren) { + children = applyStrikeThrough(children); + } + return ( + + {!hideChildren && ( + + {children} + + )} + {shouldShowErrorMessages && hasErrorMessages && ( + + )} + + ); +} + +OfflineWithFeedback.displayName = 'OfflineWithFeedback'; + +export default OfflineWithFeedback; diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index cb670f3cf6ce..8d100b466d1b 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -5,8 +5,8 @@ import {InteractionManager, StyleSheet, View} from 'react-native'; import _ from 'underscore'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Button from './Button'; @@ -104,6 +104,7 @@ const defaultProps = { function OptionRow(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const pressableRef = useRef(null); const [isDisabled, setIsDisabled] = useState(props.isDisabled); diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index b022823d215a..30d5e4c48d68 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -7,10 +7,10 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import Text from '@components/Text'; import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState'; import withLocalize from '@components/withLocalize'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import withWindowDimensions from '@components/withWindowDimensions'; import compose from '@libs/compose'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; @@ -19,6 +19,7 @@ const propTypes = { ...pdfViewPropTypes, ...keyboardStatePropTypes, ...withThemeStylesPropTypes, + ...withStyleUtilsPropTypes, }; /** @@ -128,7 +129,7 @@ class PDFView extends Component { } renderPDFView() { - const pdfStyles = [this.props.themeStyles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(this.props.windowWidth, this.props.windowHeight)]; + const pdfStyles = [this.props.themeStyles.imageModalPDF, this.props.StyleUtils.getWidthAndHeightStyle(this.props.windowWidth, this.props.windowHeight)]; // If we haven't yet successfully validated the password and loaded the PDF, // then we need to hide the react-native-pdf/PDF component so that PDFPasswordForm @@ -199,4 +200,4 @@ class PDFView extends Component { PDFView.propTypes = propTypes; PDFView.defaultProps = defaultProps; -export default compose(withWindowDimensions, withKeyboardState, withLocalize, withThemeStyles)(PDFView); +export default compose(withWindowDimensions, withKeyboardState, withLocalize, withThemeStyles, withStyleUtils)(PDFView); diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js index 24a0bdb70903..b2c94c81770f 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.js @@ -3,7 +3,7 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {computeHorizontalShift, computeVerticalShift} from '@styles/getPopoverWithMeasuredContentStyles'; +import PopoverWithMeasuredContentStyleUtils from '@styles/PopoverWithMeasuredContentStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Popover from './Popover'; @@ -128,8 +128,8 @@ function PopoverWithMeasuredContent(props) { }; }, [props.anchorPosition, props.anchorAlignment, popoverWidth, popoverHeight]); - const horizontalShift = computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth); - const verticalShift = computeVerticalShift(adjustedAnchorPosition.top, popoverHeight, windowHeight); + const horizontalShift = PopoverWithMeasuredContentStyleUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth); + const verticalShift = PopoverWithMeasuredContentStyleUtils.computeVerticalShift(adjustedAnchorPosition.top, popoverHeight, windowHeight); const shiftedAnchorPosition = { left: adjustedAnchorPosition.left + horizontalShift, bottom: windowHeight - (adjustedAnchorPosition.top + popoverHeight) - verticalShift, diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 6572a55ed889..c13d9e1a0931 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -5,37 +5,34 @@ import {defaultProps, propTypes} from '@components/Popover/popoverPropTypes'; import {PopoverContext} from '@components/PopoverProvider'; import withWindowDimensions from '@components/withWindowDimensions'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import getModalStyles from '@styles/getModalStyles'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Modal from '@userActions/Modal'; function Popover(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {onOpen, close} = React.useContext(PopoverContext); const insets = useSafeAreaInsets(); - const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles( - theme, - styles, - 'popover', - { - windowWidth: props.windowWidth, - windowHeight: props.windowHeight, - isSmallScreenWidth: false, - }, - props.anchorPosition, - props.innerContainerStyle, - props.outerStyle, - ); + const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = + StyleUtils.getModalStyles( + 'popover', + { + windowWidth: props.windowWidth, + windowHeight: props.windowHeight, + isSmallScreenWidth: false, + }, + props.anchorPosition, + props.innerContainerStyle, + props.outerStyle, + ); const { paddingTop: safeAreaPaddingTop, paddingBottom: safeAreaPaddingBottom, paddingLeft: safeAreaPaddingLeft, paddingRight: safeAreaPaddingRight, - } = useMemo(() => StyleUtils.getSafeAreaPadding(insets), [insets]); + } = useMemo(() => StyleUtils.getSafeAreaPadding(insets), [StyleUtils, insets]); const modalPaddingStyles = useMemo( () => @@ -55,6 +52,7 @@ function Popover(props) { insets, }), [ + StyleUtils, insets, modalContainerStyle.marginBottom, modalContainerStyle.marginTop, diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index c0bb531db007..0d0ed33d5138 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -5,7 +5,7 @@ import useSingleExecution from '@hooks/useSingleExecution'; import Accessibility from '@libs/Accessibility'; import HapticFeedback from '@libs/HapticFeedback'; import KeyboardShortcut from '@libs/KeyboardShortcut'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import PressableProps, {PressableRef} from './types'; @@ -37,6 +37,7 @@ function GenericPressable( ref: PressableRef, ) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {isExecuting, singleExecution} = useSingleExecution(); const isScreenReaderActive = Accessibility.useScreenReaderStatus(); const [hitSlop, onLayout] = Accessibility.useAutoHitSlop(); diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index a80272ee05cf..6237ce3e4660 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -8,8 +8,7 @@ import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import PressableProps, {PressableRef} from './GenericPressable/types'; @@ -67,8 +66,8 @@ function PressableWithDelayToggle( }: PressableWithDelayToggleProps, ref: PressableRef, ) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isActive, temporarilyDisableInteractions] = useThrottledButtonState(); const updatePressState = () => { @@ -122,7 +121,7 @@ function PressableWithDelayToggle( {icon && ( (null); const executeSecondaryInteraction = (event: GestureResponderEvent) => { diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.js index 78c1f5407d64..994d467dfd6e 100644 --- a/src/components/Reactions/AddReactionBubble.js +++ b/src/components/Reactions/AddReactionBubble.js @@ -8,8 +8,7 @@ import Text from '@components/Text'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; @@ -55,8 +54,8 @@ const defaultProps = { }; function AddReactionBubble(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const ref = useRef(); useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); @@ -89,11 +88,7 @@ function AddReactionBubble(props) { [ - styles.emojiReactionBubble, - styles.userSelectNone, - StyleUtils.getEmojiReactionBubbleStyle(theme, hovered || pressed, false, props.isContextMenu), - ]} + style={({hovered, pressed}) => [styles.emojiReactionBubble, styles.userSelectNone, StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, false, props.isContextMenu)]} onPress={Session.checkIfActionIsAllowed(onPress)} onMouseDown={(e) => { // Allow text input blur when Add reaction is right clicked @@ -121,7 +116,7 @@ function AddReactionBubble(props) { src={Expensicons.AddReaction} width={props.isContextMenu ? variables.iconSizeNormal : variables.iconSizeSmall} height={props.isContextMenu ? variables.iconSizeNormal : variables.iconSizeSmall} - fill={StyleUtils.getIconFillColor(theme, getButtonState(hovered, pressed))} + fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed))} /> diff --git a/src/components/Reactions/EmojiReactionBubble.js b/src/components/Reactions/EmojiReactionBubble.js index 73538a032e38..7fcdae8c0a5a 100644 --- a/src/components/Reactions/EmojiReactionBubble.js +++ b/src/components/Reactions/EmojiReactionBubble.js @@ -4,8 +4,7 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar import Text from '@components/Text'; import {withCurrentUserPersonalDetailsDefaultProps} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -55,13 +54,13 @@ const defaultProps = { }; function EmojiReactionBubble(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( [ styles.emojiReactionBubble, - StyleUtils.getEmojiReactionBubbleStyle(theme, hovered || pressed, props.hasUserReacted, props.isContextMenu), + StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, props.hasUserReacted, props.isContextMenu), props.shouldBlockReactions && styles.cursorDisabled, styles.userSelectNone, ]} @@ -89,7 +88,7 @@ function EmojiReactionBubble(props) { dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > {props.emojiCodes.join('')} - {props.count > 0 && {props.count}} + {props.count > 0 && {props.count}} ); } diff --git a/src/components/Reactions/MiniQuickEmojiReactions.js b/src/components/Reactions/MiniQuickEmojiReactions.js index 7795f77d5d53..92913a7c4c5e 100644 --- a/src/components/Reactions/MiniQuickEmojiReactions.js +++ b/src/components/Reactions/MiniQuickEmojiReactions.js @@ -11,8 +11,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as Session from '@userActions/Session'; @@ -56,8 +55,8 @@ const defaultProps = { * @returns {JSX.Element} */ function MiniQuickEmojiReactions(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const ref = useRef(); const openEmojiPicker = () => { @@ -107,7 +106,7 @@ function MiniQuickEmojiReactions(props) { )} diff --git a/src/components/ReportActionItem/MoneyReportView.js b/src/components/ReportActionItem/MoneyReportView.js index c870f11d6f3c..c52d4abe371c 100644 --- a/src/components/ReportActionItem/MoneyReportView.js +++ b/src/components/ReportActionItem/MoneyReportView.js @@ -11,8 +11,8 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import reportPropTypes from '@pages/reportPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; @@ -29,6 +29,7 @@ const propTypes = { function MoneyReportView(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const isSettled = ReportUtils.isSettled(props.report.reportID); diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 7391b8ccd933..8dcdb32e530a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -28,12 +28,13 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import walletTermsPropTypes from '@pages/EnablePayments/walletTermsPropTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import ReportActionItemImages from './ReportActionItemImages'; @@ -139,6 +140,7 @@ const defaultProps = { function MoneyRequestPreview(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); if (_.isEmpty(props.iouReport) && !props.isBillSplit) { @@ -234,6 +236,10 @@ function MoneyRequestPreview(props) { return props.translate('iou.receiptScanning'); } + if (TransactionUtils.hasMissingSmartscanFields(props.transaction)) { + return Localize.translateLocal('iou.receiptMissingDetails'); + } + return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); }; diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 80725a1e2531..f26350d70b3f 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -32,8 +32,8 @@ import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateB import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import iouReportPropTypes from '@pages/iouReportPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -82,6 +82,7 @@ const defaultProps = { function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy}) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); diff --git a/src/components/ReportActionItem/ReportActionItemImages.js b/src/components/ReportActionItem/ReportActionItemImages.js index de9ff3b6c30b..b20513d63fe3 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.js +++ b/src/components/ReportActionItem/ReportActionItemImages.js @@ -5,8 +5,8 @@ import {Polygon, Svg} from 'react-native-svg'; import _ from 'underscore'; import Text from '@components/Text'; import transactionPropTypes from '@components/transactionPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import ReportActionItemImage from './ReportActionItemImage'; @@ -53,6 +53,7 @@ const defaultProps = { function ReportActionItemImages({images, size, total, isHovered}) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); // Calculate the number of images to be shown, limited by the value of 'size' (if defined) // or the total number of images. const numberOfShownImages = Math.min(size || images.length, images.length); diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index abc65b513ab9..5d1c9972666a 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -22,8 +22,7 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; @@ -76,8 +75,8 @@ const defaultProps = { }; function TaskPreview(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; // The reportAction might not contain details regarding the taskReport // Only the direct parent reportAction will contain details about the taskReport @@ -130,7 +129,7 @@ function TaskPreview(props) { diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js index 3d3ed9315f6e..ea02dba705a6 100644 --- a/src/components/ReportActionItem/TaskView.js +++ b/src/components/ReportActionItem/TaskView.js @@ -24,8 +24,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; @@ -46,8 +45,8 @@ const propTypes = { }; function TaskView(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); useEffect(() => { Task.setTaskReport({...props.report}); }, [props.report]); @@ -85,7 +84,7 @@ function TaskView(props) { style={({pressed}) => [ styles.ph5, styles.pv2, - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(hovered, pressed, false, disableState, !isDisableInteractive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState, !isDisableInteractive), true), isDisableInteractive && !disableState && styles.cursorDefault, ]} ref={props.forwardedRef} @@ -125,7 +124,7 @@ function TaskView(props) { )} diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index 4a1d60a869ad..ff6a3c4562b3 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -3,8 +3,8 @@ import React, {memo} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import * as UserUtils from '@libs/UserUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import AttachmentModal from './AttachmentModal'; @@ -24,6 +24,7 @@ const defaultProps = { function RoomHeaderAvatars(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); if (!props.icons.length) { return null; } @@ -33,6 +34,7 @@ function RoomHeaderAvatars(props) { @@ -64,7 +66,7 @@ function RoomHeaderAvatars(props) { styles.roomHeaderAvatar, // Due to border-box box-sizing, the Avatars have to be larger when bordered to visually match size with non-bordered Avatars - StyleUtils.getAvatarStyle(theme, CONST.AVATAR_SIZE.LARGE_BORDERED), + StyleUtils.getAvatarStyle(CONST.AVATAR_SIZE.LARGE_BORDERED), ]; return ( @@ -77,6 +79,7 @@ function RoomHeaderAvatars(props) { diff --git a/src/components/SafeAreaConsumer/index.android.tsx b/src/components/SafeAreaConsumer/index.android.tsx index 472c2f037d29..5117000627cf 100644 --- a/src/components/SafeAreaConsumer/index.android.tsx +++ b/src/components/SafeAreaConsumer/index.android.tsx @@ -2,7 +2,7 @@ import React from 'react'; // eslint-disable-next-line no-restricted-imports import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; import StatusBar from '@libs/StatusBar'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import SafeAreaConsumerProps from './types'; /** @@ -10,6 +10,8 @@ import SafeAreaConsumerProps from './types'; * may need not just the insets, but the computed styles so we save a few lines of code with this. */ function SafeAreaConsumer({children}: SafeAreaConsumerProps) { + const StyleUtils = useStyleUtils(); + return ( {(insets) => { diff --git a/src/components/SafeAreaConsumer/index.tsx b/src/components/SafeAreaConsumer/index.tsx index f515d6697bb4..54c5d984be5f 100644 --- a/src/components/SafeAreaConsumer/index.tsx +++ b/src/components/SafeAreaConsumer/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; // eslint-disable-next-line no-restricted-imports import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import SafeAreaConsumerProps from './types'; /** @@ -9,6 +9,8 @@ import SafeAreaConsumerProps from './types'; * may need not just the insets, but the computed styles so we save a few lines of code with this. */ function SafeAreaConsumer({children}: SafeAreaConsumerProps) { + const StyleUtils = useStyleUtils(); + return ( {(insets) => { diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js index f1c189842c28..c2e468359f3f 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.js @@ -7,8 +7,8 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import RadioListItem from './RadioListItem'; @@ -28,6 +28,7 @@ function BaseListItem({ }) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const isUserItem = lodashGet(item, 'icons.length', 0) > 0; const ListItem = isUserItem ? UserListItem : RadioListItem; @@ -64,7 +65,7 @@ function BaseListItem({ - {item.text} + + + {item.text} + + {Boolean(item.alternateText) && ( - {item.alternateText} + + + {item.alternateText} + + )} ); diff --git a/src/components/SelectionList/UserListItem.js b/src/components/SelectionList/UserListItem.js index 0e3af0ee10f9..0b4b6b52e0ac 100644 --- a/src/components/SelectionList/UserListItem.js +++ b/src/components/SelectionList/UserListItem.js @@ -7,7 +7,7 @@ import Tooltip from '@components/Tooltip'; import useThemeStyles from '@styles/useThemeStyles'; import {userListItemPropTypes} from './selectionListPropTypes'; -function UserListItem({item, isFocused = false, showTooltip}) { +function UserListItem({item, textStyles, alternateTextStyles, showTooltip}) { const styles = useThemeStyles(); return ( <> @@ -24,7 +24,7 @@ function UserListItem({item, isFocused = false, showTooltip}) { text={item.text} > {item.text} @@ -36,7 +36,7 @@ function UserListItem({item, isFocused = false, showTooltip}) { text={item.alternateText} > {item.alternateText} diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index 0c2fe83d025f..bd434cf87716 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -6,6 +6,12 @@ const commonListItemPropTypes = { /** Whether this item is focused (for arrow key controls) */ isFocused: PropTypes.bool, + /** Style to be applied to Text */ + textStyles: PropTypes.arrayOf(PropTypes.object), + + /** Style to be applied on the alternate text */ + alternateTextStyles: PropTypes.arrayOf(PropTypes.object), + /** Whether this item is disabled */ isDisabled: PropTypes.bool, diff --git a/src/components/SpacerView.js b/src/components/SpacerView.js index 9509219c0ac7..b9705a15b5f3 100644 --- a/src/components/SpacerView.js +++ b/src/components/SpacerView.js @@ -3,7 +3,7 @@ import React from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import usePrevious from '@hooks/usePrevious'; import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import CONST from '@src/CONST'; const propTypes = { @@ -24,6 +24,7 @@ const defaultProps = { }; function SpacerView({shouldShow = true, style = []}) { + const StyleUtils = useStyleUtils(); const marginVertical = useSharedValue(shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_MARGIN_VERTICAL : CONST.HORIZONTAL_SPACER.HIDDEN_MARGIN_VERTICAL); const borderBottomWidth = useSharedValue(shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_BORDER_BOTTOM_WIDTH : CONST.HORIZONTAL_SPACER.HIDDEN_BORDER_BOTTOM_WIDTH); const prevShouldShow = usePrevious(shouldShow); diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 52ecf6633d9b..7fa2900eaffb 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -2,8 +2,8 @@ import React, {memo} from 'react'; import {View} from 'react-native'; import {ValueOf} from 'type-fest'; import type {AvatarSource} from '@libs/UserUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import {AvatarType} from '@src/types/onyx/OnyxCommon'; @@ -50,9 +50,10 @@ type SubscriptAvatarProps = { function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AVATAR_SIZE.DEFAULT, backgroundColor, noMargin = false, showTooltip = true}: SubscriptAvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const isSmall = size === CONST.AVATAR_SIZE.SMALL; const subscriptStyle = size === CONST.AVATAR_SIZE.SMALL_NORMAL ? styles.secondAvatarSubscriptSmallNormal : styles.secondAvatarSubscript; - const containerStyle = StyleUtils.getContainerStyles(styles, size); + const containerStyle = StyleUtils.getContainerStyles(size); return ( diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js index 78724718b2af..334ca3e38370 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.js @@ -6,7 +6,7 @@ import OptionsSelector from '@components/OptionsSelector'; import useLocalize from '@hooks/useLocalize'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -14,6 +14,7 @@ import {defaultProps, propTypes} from './tagPickerPropTypes'; function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption, insets, onSubmit}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 96a6f535877a..ae2f6ba99392 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,30 +1,32 @@ import React, {ForwardedRef} from 'react'; -// eslint-disable-next-line no-restricted-imports import {Text as RNText, TextProps as RNTextProps, StyleSheet} from 'react-native'; import type {TextStyle} from 'react-native'; import fontFamily from '@styles/fontFamily'; import useTheme from '@styles/themes/useTheme'; import variables from '@styles/variables'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; -type TextProps = RNTextProps & { - /** The color of the text */ - color?: string; +type TextProps = RNTextProps & + ChildrenProps & { + /** The color of the text */ + color?: string; - /** The size of the text */ - fontSize?: number; + /** The size of the text */ + fontSize?: number; - /** The alignment of the text */ - textAlign?: 'left' | 'right' | 'auto' | 'center' | 'justify'; + /** The alignment of the text */ + textAlign?: TextStyle['textAlign']; - /** Any children to display */ - children: React.ReactNode; + /** Any children to display */ + children: React.ReactNode; - /** The family of the font to use */ - family?: keyof typeof fontFamily; -}; + /** The family of the font to use */ + family?: keyof typeof fontFamily; + }; -function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children = null, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { +function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { const theme = useTheme(); + const componentStyle: TextStyle = { color: color ?? theme.text, fontSize, @@ -36,6 +38,7 @@ function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', c if (!componentStyle.lineHeight && componentStyle.fontSize === variables.fontSizeNormal) { componentStyle.lineHeight = variables.fontSizeNormalHeight; } + return ( 0 || Boolean(props.prefixCharacter); @@ -333,7 +334,7 @@ function BaseTextInput(props) { !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - ...(props.autoGrowHeight ? [StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), styles.verticalAlignTop] : []), + ...(props.autoGrowHeight ? [StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), styles.verticalAlignTop] : []), // Add disabled color theme when field is not editable. props.disabled && styles.textInputDisabled, diff --git a/src/components/TextInput/BaseTextInput/index.native.js b/src/components/TextInput/BaseTextInput/index.native.js index 190104198986..b9bbf567f9cb 100644 --- a/src/components/TextInput/BaseTextInput/index.native.js +++ b/src/components/TextInput/BaseTextInput/index.native.js @@ -16,8 +16,8 @@ import withLocalize from '@components/withLocalize'; import getSecureEntryKeyboardType from '@libs/getSecureEntryKeyboardType'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import useNativeDriver from '@libs/useNativeDriver'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -26,6 +26,7 @@ import * as baseTextInputPropTypes from './baseTextInputPropTypes'; function BaseTextInput(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const initialValue = props.value || props.defaultValue || ''; const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const isMultiline = props.multiline || props.autoGrowHeight; @@ -312,7 +313,7 @@ function BaseTextInput(props) { !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - ...(props.autoGrowHeight ? [StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), styles.verticalAlignTop] : []), + ...(props.autoGrowHeight ? [StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), styles.verticalAlignTop] : []), // Add disabled color theme when field is not editable. props.disabled && styles.textInputDisabled, styles.pointerEventsAuto, diff --git a/src/components/TextLink.js b/src/components/TextLink.js deleted file mode 100644 index 46c074eb79e6..000000000000 --- a/src/components/TextLink.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import useEnvironment from '@hooks/useEnvironment'; -import stylePropTypes from '@styles/stylePropTypes'; -import useThemeStyles from '@styles/useThemeStyles'; -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; -import refPropTypes from './refPropTypes'; -import Text from './Text'; - -const propTypes = { - /** Link to open in new tab */ - href: PropTypes.string, - - /** Text content child */ - children: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]).isRequired, - - /** Additional style props */ - style: stylePropTypes, - - /** Overwrites the default link behavior with a custom callback */ - onPress: PropTypes.func, - - /** Callback that is called when mousedown is triggered */ - onMouseDown: PropTypes.func, - - /** A ref to forward to text */ - forwardedRef: refPropTypes, -}; - -const defaultProps = { - forwardedRef: undefined, - href: undefined, - style: [], - onPress: undefined, - onMouseDown: (event) => event.preventDefault(), -}; - -function TextLink(props) { - const {environmentURL} = useEnvironment(); - const styles = useThemeStyles(); - const rest = _.omit(props, _.keys(propTypes)); - const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; - - /** - * @param {Event} event - */ - const openLink = (event) => { - event.preventDefault(); - if (props.onPress) { - props.onPress(); - return; - } - - Link.openLink(props.href, environmentURL); - }; - - /** - * @param {Event} event - */ - const openLinkIfEnterKeyPressed = (event) => { - if (event.key !== 'Enter') { - return; - } - openLink(event); - }; - - return ( - - {props.children} - - ); -} - -TextLink.defaultProps = defaultProps; -TextLink.propTypes = propTypes; -TextLink.displayName = 'TextLink'; - -const TextLinkWithRef = React.forwardRef((props, ref) => ( - -)); - -TextLinkWithRef.displayName = 'TextLinkWithRef'; - -export default TextLinkWithRef; diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx new file mode 100644 index 000000000000..95c456ddc8e3 --- /dev/null +++ b/src/components/TextLink.tsx @@ -0,0 +1,79 @@ +import React, {ForwardedRef, forwardRef, KeyboardEventHandler, MouseEventHandler} from 'react'; +import {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native'; +import useEnvironment from '@hooks/useEnvironment'; +import useThemeStyles from '@styles/useThemeStyles'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import Text, {TextProps} from './Text'; + +type LinkProps = { + /** Link to open in new tab */ + href: string; + + onPress?: undefined; +}; + +type PressProps = { + href?: undefined; + + /** Overwrites the default link behavior with a custom callback */ + onPress: () => void; +}; + +type TextLinkProps = (LinkProps | PressProps) & + TextProps & { + /** Additional style props */ + style?: StyleProp; + + /** Callback that is called when mousedown is triggered */ + onMouseDown?: MouseEventHandler; + }; + +function TextLink({href, onPress, children, style, onMouseDown = (event) => event.preventDefault(), ...rest}: TextLinkProps, ref: ForwardedRef) { + const {environmentURL} = useEnvironment(); + const styles = useThemeStyles(); + + const openLink = () => { + if (onPress) { + onPress(); + } else { + Link.openLink(href, environmentURL); + } + }; + + const openLinkOnTap = (event: GestureResponderEvent) => { + event.preventDefault(); + + openLink(); + }; + + const openLinkOnEnterKey: KeyboardEventHandler = (event) => { + if (event.key !== 'Enter') { + return; + } + event.preventDefault(); + + openLink(); + }; + + return ( + + {children} + + ); +} + +TextLink.displayName = 'TextLink'; + +export default forwardRef(TextLink); diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 0fdd626a1517..a4a200d96082 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useState} from 'react'; import {Dimensions, StyleProp, View, ViewStyle} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import ImageWithSizeCalculation from './ImageWithSizeCalculation'; @@ -72,6 +72,7 @@ function calculateThumbnailImageSize(width: number, height: number, windowHeight function ThumbnailImage({previewSourceURL, style, isAuthTokenRequired, imageWidth = 200, imageHeight = 200, shouldDynamicallyResize = true}: ThumbnailImageProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {windowHeight} = useWindowDimensions(); const initialDimensions = calculateThumbnailImageSize(imageWidth, imageHeight, windowHeight); const [currentImageWidth, setCurrentImageWidth] = useState(initialDimensions.thumbnailWidth); diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.js b/src/components/Tooltip/TooltipRenderedOnPageBody.js index d92457238675..10e82cc94c30 100644 --- a/src/components/Tooltip/TooltipRenderedOnPageBody.js +++ b/src/components/Tooltip/TooltipRenderedOnPageBody.js @@ -4,9 +4,7 @@ import ReactDOM from 'react-dom'; import {Animated, View} from 'react-native'; import Text from '@components/Text'; import Log from '@libs/Log'; -import getTooltipStyles from '@styles/getTooltipStyles'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; +import useStyleUtils from '@styles/useStyleUtils'; const propTypes = { /** Window width */ @@ -84,8 +82,7 @@ function TooltipRenderedOnPageBody({ const contentRef = useRef(); const rootWrapper = useRef(); - const theme = useTheme(); - const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); useEffect(() => { if (!renderTooltipContent || !text) { @@ -103,7 +100,7 @@ function TooltipRenderedOnPageBody({ const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( () => - getTooltipStyles({ + StyleUtils.getTooltipStyles({ tooltip: rootWrapper.current, currentSize: animation, windowWidth, @@ -114,12 +111,10 @@ function TooltipRenderedOnPageBody({ maxWidth, tooltipContentWidth: contentMeasuredWidth, tooltipWrapperHeight: wrapperMeasuredHeight, - theme, - styles, shiftHorizontal, shiftVertical, }), - [animation, windowWidth, xOffset, yOffset, targetWidth, targetHeight, maxWidth, contentMeasuredWidth, wrapperMeasuredHeight, shiftHorizontal, shiftVertical, theme, styles], + [StyleUtils, animation, windowWidth, xOffset, yOffset, targetWidth, targetHeight, maxWidth, contentMeasuredWidth, wrapperMeasuredHeight, shiftHorizontal, shiftVertical], ); let content; diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip.web.js b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip.website.js similarity index 100% rename from src/components/UserDetailsTooltip/BaseUserDetailsTooltip.web.js rename to src/components/UserDetailsTooltip/BaseUserDetailsTooltip.website.js diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js index 35b8d2babdb0..6eb20d1bde6a 100644 --- a/src/components/ValuePicker/index.js +++ b/src/components/ValuePicker/index.js @@ -5,7 +5,7 @@ import {View} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import ValueSelectorModal from './ValueSelectorModal'; @@ -45,6 +45,7 @@ const defaultProps = { function ValuePicker({value, label, items, placeholder, errorText, onInputChange, forwardedRef}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isPickerVisible, setIsPickerVisible] = useState(false); const showPickerModal = () => { diff --git a/src/components/withStyleUtils.tsx b/src/components/withStyleUtils.tsx new file mode 100644 index 000000000000..6ea044fce70c --- /dev/null +++ b/src/components/withStyleUtils.tsx @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React, {ComponentType, ForwardedRef, forwardRef, ReactElement, RefAttributes} from 'react'; +import getComponentDisplayName from '@libs/getComponentDisplayName'; +import useStyleUtils from '@styles/useStyleUtils'; +import {StyleUtilsType} from '@styles/utils'; + +const withStyleUtilsPropTypes = { + StyleUtils: PropTypes.object.isRequired, +}; +type WithStyleUtilsProps = {StyleUtils: StyleUtilsType}; + +export default function withStyleUtils( + WrappedComponent: ComponentType>, +): (props: Omit & React.RefAttributes) => ReactElement | null { + function WithStyleUtils(props: Omit, ref: ForwardedRef): ReactElement { + const StyleUtils = useStyleUtils(); + return ( + + ); + } + + WithStyleUtils.displayName = `withStyleUtils(${getComponentDisplayName(WrappedComponent)})`; + + return forwardRef(WithStyleUtils); +} + +export {withStyleUtilsPropTypes, type WithStyleUtilsProps}; diff --git a/src/components/withTheme.tsx b/src/components/withTheme.tsx index 532ff6e5c375..59380110904a 100644 --- a/src/components/withTheme.tsx +++ b/src/components/withTheme.tsx @@ -7,12 +7,12 @@ import useTheme from '@styles/themes/useTheme'; const withThemePropTypes = { theme: PropTypes.object.isRequired, }; -type ThemeProps = {theme: ThemeColors}; +type WithThemeProps = {theme: ThemeColors}; -export default function withTheme( +export default function withTheme( WrappedComponent: ComponentType>, -): (props: Omit & React.RefAttributes) => ReactElement | null { - function WithTheme(props: Omit, ref: ForwardedRef): ReactElement { +): (props: Omit & React.RefAttributes) => ReactElement | null { + function WithTheme(props: Omit, ref: ForwardedRef): ReactElement { const theme = useTheme(); return ( ( return forwardRef(WithTheme); } -export {withThemePropTypes, type ThemeProps}; +export {withThemePropTypes}; +export type {WithThemeProps}; diff --git a/src/components/withThemeStyles.tsx b/src/components/withThemeStyles.tsx index bdd5e50fe8e3..d811573d1730 100644 --- a/src/components/withThemeStyles.tsx +++ b/src/components/withThemeStyles.tsx @@ -7,12 +7,12 @@ import useThemeStyles from '@styles/useThemeStyles'; const withThemeStylesPropTypes = { themeStyles: PropTypes.object.isRequired, }; -type ThemeStylesProps = {themeStyles: ThemeStyles}; +type WithThemeStylesProps = {themeStyles: ThemeStyles}; -export default function withThemeStyles( +export default function withThemeStyles( WrappedComponent: ComponentType>, -): (props: Omit & React.RefAttributes) => ReactElement | null { - function WithThemeStyles(props: Omit, ref: ForwardedRef): ReactElement { +): (props: Omit & React.RefAttributes) => ReactElement | null { + function WithThemeStyles(props: Omit, ref: ForwardedRef): ReactElement { const themeStyles = useThemeStyles(); return ( ( return forwardRef(WithThemeStyles); } -export {withThemeStylesPropTypes, type ThemeStylesProps}; +export {withThemeStylesPropTypes}; +export type {WithThemeStylesProps}; diff --git a/src/languages/en.ts b/src/languages/en.ts index 817f06f6b344..63d68b8e19dd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1190,8 +1190,10 @@ export default { toGetStarted: 'Add a bank account and issue corporate cards, reimburse expenses, collect invoice payments, and pay bills, all from one place.', plaidBodyCopy: 'Give your employees an easier way to pay - and get paid back - for company expenses.', checkHelpLine: 'Your routing number and account number can be found on a check for the account.', - validateAccountError: - 'In order to finish setting up your bank account, you must validate your account. Please check your email to validate your account, and return here to finish up!', + validateAccountError: { + phrase1: 'Hold up! We need you to validate your account first. To do so, ', + phrase2: 'sign back in with a magic code', + }, hasPhoneLoginError: 'To add a verified bank account please ensure your primary login is a valid email and try again. You can add your phone number as a secondary login.', hasBeenThrottledError: 'There was an error adding your bank account. Please wait a few minutes and try again.', hasCurrencyError: 'Oops! It appears that your workspace currency is set to a different currency than USD. To proceed, please set it to USD and try again', diff --git a/src/languages/es.ts b/src/languages/es.ts index b219021daa0f..0b63dea533f9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1205,8 +1205,10 @@ export default { toGetStarted: 'Añade una cuenta bancaria y emite tarjetas corporativas, reembolsa gastos y cobra y paga facturas, todo desde un mismo lugar.', plaidBodyCopy: 'Ofrezca a sus empleados una forma más sencilla de pagar - y recuperar - los gastos de la empresa.', checkHelpLine: 'Su número de ruta y número de cuenta se pueden encontrar en un cheque de la cuenta bancaria.', - validateAccountError: - 'Para terminar de configurar tu cuenta bancaria, debes validar tu cuenta de Expensify. Por favor, revisa tu correo electrónico para validar tu cuenta y vuelve aquí para continuar.', + validateAccountError: { + phrase1: '¡Un momento! Primero necesitas validar tu cuenta. Para hacerlo, ', + phrase2: 'vuelve a iniciar sesión con un código mágico', + }, hasPhoneLoginError: 'Para añadir una cuenta bancaria verificada, asegúrate de que tu nombre de usuario principal sea un correo electrónico válido y vuelve a intentarlo. Puedes añadir tu número de teléfono como nombre de usuario secundario.', hasBeenThrottledError: 'Se produjo un error al intentar añadir tu cuenta bancaria. Por favor, espera unos minutos e inténtalo de nuevo.', diff --git a/src/libs/Browser/index.web.ts b/src/libs/Browser/index.website.ts similarity index 100% rename from src/libs/Browser/index.web.ts rename to src/libs/Browser/index.website.ts diff --git a/src/libs/EmailUtils.ts b/src/libs/EmailUtils.ts new file mode 100644 index 000000000000..886823faae87 --- /dev/null +++ b/src/libs/EmailUtils.ts @@ -0,0 +1,30 @@ +/** + * Trims the `mailto:` part from mail link. + * @param mailLink - The `mailto:` link to be trimmed + * @returns The email address + */ +function trimMailTo(mailLink: string) { + return mailLink.replace('mailto:', ''); +} + +/** + * Prepends a zero-width space (U+200B) character before all `.` and `@` characters + * in the email address to provide explicit line break opportunities for consistent + * breaking across platforms. + * + * Note: as explained [here](https://github.com/Expensify/App/issues/30985#issuecomment-1815379835), + * this only provides opportunities for line breaking (rather than forcing line breaks) that shall + * be used by the platform implementation when there are no other customary rules applicable + * and the text would otherwise overflow. + * @param email - The email address to be sanitized + * @returns The email with inserted line break opportunities + */ +function prefixMailSeparatorsWithBreakOpportunities(email: string) { + return email.replace( + /([.@])/g, + // below: zero-width space (U+200B) character + '​$1', + ); +} + +export default {trimMailTo, prefixMailSeparatorsWithBreakOpportunities}; diff --git a/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts b/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts index 6b56bb00cf56..6cae31a219f9 100644 --- a/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts @@ -1,12 +1,12 @@ import {CardStyleInterpolators, StackNavigationOptions} from '@react-navigation/stack'; -import styles from '@styles/styles'; +import {ThemeStyles} from '@styles/styles'; /** * RHP stack navigator screen options generator function * @param themeStyles - The styles object * @returns The screen options object */ -const RHPScreenOptions = (themeStyles: typeof styles): StackNavigationOptions => ({ +const RHPScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => ({ headerShown: false, animationEnabled: true, gestureDirection: 'horizontal', diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts index 08f18ce3ab9d..9af92dd3e019 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts @@ -1,6 +1,6 @@ import {StackCardInterpolationProps, StackNavigationOptions} from '@react-navigation/stack'; import getNavigationModalCardStyle from '@styles/getNavigationModalCardStyles'; -import styles from '@styles/styles'; +import {ThemeStyles} from '@styles/styles'; import variables from '@styles/variables'; import CONFIG from '@src/CONFIG'; import modalCardStyleInterpolator from './modalCardStyleInterpolator'; @@ -15,7 +15,7 @@ const commonScreenOptions: StackNavigationOptions = { animationTypeForReplace: 'push', }; -export default (isSmallScreenWidth: boolean, themeStyles: typeof styles): ScreenOptions => ({ +export default (isSmallScreenWidth: boolean, themeStyles: ThemeStyles): ScreenOptions => ({ rightModalNavigator: { ...commonScreenOptions, cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 4678ce395f76..13586b6c5d2e 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -357,6 +357,8 @@ function getAllReportErrors(report, reportActions) { if (ReportUtils.hasMissingSmartscanFields(report.reportID) && !ReportUtils.isSettled(report.reportID)) { _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); } + } else if (ReportUtils.hasSmartscanError(_.values(reportActions))) { + _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); } // All error objects related to the report. Each object in the sources contains error messages keyed by microtime diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 6dc735ebd8b7..93d747c7018e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -126,11 +126,11 @@ function isThreadParentMessage(reportAction: OnyxEntry, reportID: * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getParentReportAction(report: OnyxEntry, allReportActionsParam?: OnyxCollection): ReportAction | Record { +function getParentReportAction(report: OnyxEntry): ReportAction | Record { if (!report?.parentReportID || !report.parentReportActionID) { return {}; } - return (allReportActionsParam ?? allReportActions)?.[report.parentReportID]?.[report.parentReportActionID] ?? {}; + return allReportActions?.[report.parentReportID]?.[report.parentReportActionID] ?? {}; } /** diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1266f145de30..608c4333ab0f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -421,6 +421,19 @@ Onyx.connect({ }, }); +let allTransactions: OnyxCollection = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + return; + } + allTransactions = Object.fromEntries(Object.entries(value).filter(([, transaction]) => transaction)); + }, +}); + function getPolicyTags(policyID: string) { return allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; } @@ -898,8 +911,8 @@ function hasSingleParticipant(report: OnyxEntry): boolean { * */ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { - const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return allTransactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); + const transactions = TransactionUtils.getAllReportTransactions(iouReportID); + return transactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); } /** @@ -1588,8 +1601,8 @@ function requiresAttentionFromCurrentUser(option: OnyxEntry | OptionData * */ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolean { - const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return allTransactions.filter((transaction) => transaction.reimbursable === false).length > 0; + const transactions = TransactionUtils.getAllReportTransactions(iouReportID); + return transactions.filter((transaction) => transaction.reimbursable === false).length > 0; } function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { @@ -1841,8 +1854,8 @@ function canEditReportAction(reportAction: OnyxEntry): boolean { * Gets all transactions on an IOU report with a receipt */ function getTransactionsWithReceipts(iouReportID: string | undefined): Transaction[] { - const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return allTransactions.filter((transaction) => TransactionUtils.hasReceipt(transaction)); + const transactions = TransactionUtils.getAllReportTransactions(iouReportID); + return transactions.filter((transaction) => TransactionUtils.hasReceipt(transaction)); } /** @@ -1934,6 +1947,10 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.receiptScanning'); } + if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { + return Localize.translateLocal('iou.receiptMissingDetails'); + } + const transactionDetails = getTransactionDetails(linkedTransaction); const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); @@ -3382,8 +3399,8 @@ function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection

, policies: OnyxCollection, betas: OnyxEntry, allReportActions?: OnyxCollection): boolean { - if (isThread(report) && ReportActionsUtils.isPendingRemove(ReportActionsUtils.getParentReportAction(report, allReportActions))) { +function canAccessReport(report: OnyxEntry, policies: OnyxCollection, betas: OnyxEntry): boolean { + if (isThread(report) && ReportActionsUtils.isPendingRemove(ReportActionsUtils.getParentReportAction(report))) { return false; } @@ -3412,15 +3429,7 @@ function shouldHideReport(report: OnyxEntry, currentReportId: string): b * This logic is very specific and the order of the logic is very important. It should fail quickly in most cases and also * filter out the majority of reports before filtering out very specific minority of reports. */ -function shouldReportBeInOptionList( - report: OnyxEntry, - currentReportId: string, - isInGSDMode: boolean, - betas: Beta[], - policies: OnyxCollection, - allReportActions?: OnyxCollection, - excludeEmptyChats = false, -) { +function shouldReportBeInOptionList(report: OnyxEntry, currentReportId: string, isInGSDMode: boolean, betas: Beta[], policies: OnyxCollection, excludeEmptyChats = false) { const isInDefaultMode = !isInGSDMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. // This can happen if data is currently loading from the server or a report is in various stages of being created. @@ -3443,7 +3452,7 @@ function shouldReportBeInOptionList( ) { return false; } - if (!canAccessReport(report, policies, betas, allReportActions)) { + if (!canAccessReport(report, policies, betas)) { return false; } @@ -4251,6 +4260,22 @@ function getRoom(type: ValueOf, policyID: string) function shouldDisableWelcomeMessage(report: OnyxEntry, policy: OnyxEntry): boolean { return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy); } +/** + * Checks if report action has error when smart scanning + */ +function hasSmartscanError(reportActions: ReportAction[]) { + return reportActions.some((action) => { + if (!ReportActionsUtils.isSplitBillAction(action) && !ReportActionsUtils.isReportPreviewAction(action)) { + return false; + } + const isReportPreviewError = ReportActionsUtils.isReportPreviewAction(action) && hasMissingSmartscanFields(ReportActionsUtils.getIOUReportIDFromReportActionPreview(action)); + const transactionID = (action.originalMessage as IOUMessage).IOUTransactionID ?? '0'; + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; + const isSplitBillError = ReportActionsUtils.isSplitBillAction(action) && TransactionUtils.hasMissingSmartscanFields(transaction as Transaction); + + return isReportPreviewError || isSplitBillError; + }); +} function shouldAutoFocusOnKeyPress(event: KeyboardEvent): boolean { if (event.key.length > 1) { @@ -4451,6 +4476,7 @@ export { shouldDisableWelcomeMessage, navigateToPrivateNotes, canEditWriteCapability, + hasSmartscanError, shouldAutoFocusOnKeyPress, }; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index bace29e06d28..8657a695c7e5 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -117,7 +117,7 @@ function getOrderedReportIDs( betas: Beta[], policies: Record, priorityMode: ValueOf, - allReportActions: OnyxCollection, + allReportActions: OnyxCollection, ): string[] { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( @@ -149,9 +149,7 @@ function getOrderedReportIDs( const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports); // Filter out all the reports that shouldn't be displayed - const reportsToDisplay = allReportsDictValues.filter((report) => - ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, allReportActions, true), - ); + const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, true)); if (reportsToDisplay.length === 0) { // Display Concierge chat report when there is no report to be displayed diff --git a/src/libs/StatusBar/index.web.ts b/src/libs/StatusBar/index.website.ts similarity index 100% rename from src/libs/StatusBar/index.web.ts rename to src/libs/StatusBar/index.website.ts diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 0adacac4035a..388020bc0d6d 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -30,6 +30,17 @@ function validateCardNumber(value: string): boolean { return sum % 10 === 0; } +/** + * Validating that this is a valid address (PO boxes are not allowed) + */ +function isValidAddress(value: string): boolean { + if (!CONST.REGEX.ANY_VALUE.test(value)) { + return false; + } + + return !CONST.REGEX.PO_BOX.test(value); +} + /** * Validate date fields */ @@ -193,6 +204,40 @@ function isValidWebsite(url: string): boolean { return new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url) && isLowerCase; } +function validateIdentity(identity: Record): Record { + const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob']; + const errors: Record = {}; + + // Check that all required fields are filled + requiredFields.forEach((fieldName) => { + if (isRequiredFulfilled(identity[fieldName])) { + return; + } + errors[fieldName] = true; + }); + + if (!isValidAddress(identity.street)) { + errors.street = true; + } + + if (!isValidZipCode(identity.zipCode)) { + errors.zipCode = true; + } + + // dob field has multiple validations/errors, we are handling it temporarily like this. + if (!isValidDate(identity.dob) || !meetsMaximumAgeRequirement(identity.dob)) { + errors.dob = true; + } else if (!meetsMinimumAgeRequirement(identity.dob)) { + errors.dobAge = true; + } + + if (!isValidSSNLastFour(identity.ssnLast4)) { + errors.ssnLast4 = true; + } + + return errors; +} + function isValidUSPhone(phoneNumber = '', isCountryCodeOptional?: boolean): boolean { const phone = phoneNumber || ''; const regionCode = isCountryCodeOptional ? CONST.COUNTRY.US : undefined; @@ -259,51 +304,6 @@ function isValidPersonName(value: string) { return /^[^\d^!#$%*=<>;{}"]+$/.test(value); } -/** - * Validating that this is a valid address (PO boxes are not allowed) - */ -function isValidAddress(value: string): boolean { - if (!isValidLegalName(value)) { - return false; - } - - return !CONST.REGEX.PO_BOX.test(value); -} - -function validateIdentity(identity: Record): Record { - const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob']; - const errors: Record = {}; - - // Check that all required fields are filled - requiredFields.forEach((fieldName) => { - if (isRequiredFulfilled(identity[fieldName])) { - return; - } - errors[fieldName] = true; - }); - - if (!isValidAddress(identity.street)) { - errors.street = true; - } - - if (!isValidZipCode(identity.zipCode)) { - errors.zipCode = true; - } - - // dob field has multiple validations/errors, we are handling it temporarily like this. - if (!isValidDate(identity.dob) || !meetsMaximumAgeRequirement(identity.dob)) { - errors.dob = true; - } else if (!meetsMinimumAgeRequirement(identity.dob)) { - errors.dobAge = true; - } - - if (!isValidSSNLastFour(identity.ssnLast4)) { - errors.ssnLast4 = true; - } - - return errors; -} - /** * Checks if the provided string includes any of the provided reserved words */ @@ -384,6 +384,7 @@ export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, getAgeRequirementError, + isValidAddress, isValidDate, isValidPastDate, isValidSecurityCode, @@ -395,6 +396,7 @@ export { getFieldRequiredErrors, isValidUSPhone, isValidWebsite, + validateIdentity, isValidTwoFactorCode, isNumericWithSpecialChars, isValidRoutingNumber, @@ -407,8 +409,6 @@ export { isValidValidateCode, isValidDisplayName, isValidLegalName, - isValidAddress, - validateIdentity, doesContainReservedWord, isNumeric, isValidAccountRoute, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index e5bac3182b0d..ec34cfca0b62 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -2002,6 +2002,14 @@ function openReportFromDeepLink(url, isAuthenticated) { Session.signOutAndRedirectToSignIn(); return; } + + // We don't want to navigate to the exitTo route when creating a new workspace from a deep link, + // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate, + // which is already called when AuthScreens mounts. + if (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) { + return; + } + Navigation.navigate(route, CONST.NAVIGATION.ACTION_TYPE.PUSH); }); }); diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 6d3f0198bbfe..66345107dbb1 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -134,6 +134,7 @@ function DetailsPage(props) { {({show}) => ( diff --git a/src/pages/ErrorPage/GenericErrorPage.js b/src/pages/ErrorPage/GenericErrorPage.js index ba9bd783752b..01c4990cbc62 100644 --- a/src/pages/ErrorPage/GenericErrorPage.js +++ b/src/pages/ErrorPage/GenericErrorPage.js @@ -9,8 +9,8 @@ import SafeAreaConsumer from '@components/SafeAreaConsumer'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -24,12 +24,13 @@ const propTypes = { function GenericErrorPage({translate}) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {resetBoundary} = useErrorBoundary(); return ( {({paddingBottom}) => ( - + diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js index df38c28e561a..9609f3f9bd56 100644 --- a/src/pages/LogOutPreviousUserPage.js +++ b/src/pages/LogOutPreviousUserPage.js @@ -8,6 +8,7 @@ import * as SessionUtils from '@libs/SessionUtils'; import Navigation from '@navigation/Navigation'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; const propTypes = { /** The details about the account that the user is signing in with */ @@ -54,7 +55,10 @@ function LogOutPreviousUserPage(props) { } const exitTo = lodashGet(props, 'route.params.exitTo', ''); - if (exitTo && !props.account.isLoading && !isLoggingInAsNewUser) { + // We don't want to navigate to the exitTo route when creating a new workspace from a deep link, + // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate, + // which is already called when AuthScreens mounts. + if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !props.account.isLoading && !isLoggingInAsNewUser) { Navigation.isNavigationReady().then(() => { Navigation.navigate(exitTo); }); diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index ece75b7f6918..97ec3f99da3c 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -159,6 +159,7 @@ function ProfilePage(props) { diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index 13d6fe8568fa..2cdfd009d1af 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -1,6 +1,5 @@ import lodashGet from 'lodash/get'; import React, {useCallback} from 'react'; -import {Image} from 'react-native'; import _ from 'underscore'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; import FormProvider from '@components/Form/FormProvider'; @@ -18,7 +17,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import * as BankAccounts from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import exampleCheckImage from './exampleCheckImage'; +import ExampleCheck from './ExampleCheck'; import StepPropTypes from './StepPropTypes'; const propTypes = { @@ -27,7 +26,7 @@ const propTypes = { function BankAccountManualStep(props) { const styles = useThemeStyles(); - const {translate, preferredLocale} = useLocalize(); + const {translate} = useLocalize(); const {reimbursementAccount, reimbursementAccountDraft} = props; const shouldDisableInputs = Boolean(lodashGet(reimbursementAccount, 'achData.bankAccountID')); @@ -95,11 +94,7 @@ function BankAccountManualStep(props) { style={[styles.mh5, styles.mt3, styles.flexGrow1]} > {translate('bankAccount.checkHelpLine')} - + - {props.translate('bankAccount.validateAccountError')} + + {props.translate('bankAccount.validateAccountError.phrase1')} + + {props.translate('bankAccount.validateAccountError.phrase2')} + + . + )} diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index d7d622d309d6..f1d62eef89ae 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -88,10 +88,6 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul ]; const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); - if (values.companyName && !ValidationUtils.isValidLegalName(values.companyName)) { - errors.companyName = 'bankAccount.error.companyName'; - } - if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { errors.addressStreet = 'bankAccount.error.addressStreet'; } @@ -100,10 +96,6 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul errors.addressZipCode = 'bankAccount.error.zipCode'; } - if (values.addressCity && !ValidationUtils.isValidLegalName(values.addressCity)) { - errors.addressCity = 'bankAccount.error.addressCity'; - } - if (values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) { errors.companyPhone = 'bankAccount.error.phoneNumber'; } diff --git a/src/pages/ReimbursementAccount/ExampleCheck.js b/src/pages/ReimbursementAccount/ExampleCheck.js new file mode 100644 index 000000000000..aa32caf8144d --- /dev/null +++ b/src/pages/ReimbursementAccount/ExampleCheck.js @@ -0,0 +1,24 @@ +import React from 'react'; +import {Image} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeIllustrations from '@styles/illustrations/useThemeIllustrations'; +import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; + +function ExampleCheckImage() { + const styles = useThemeStyles(); + const illustrations = useThemeIllustrations(); + const {preferredLocale} = useLocalize(); + const isSpanish = (preferredLocale || CONST.LOCALES.DEFAULT) === CONST.LOCALES.ES; + + return ( + + ); +} + +ExampleCheckImage.displayName = 'ExampleCheckImage'; +export default ExampleCheckImage; diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js index b64f8e098a8a..f9e2058ef327 100644 --- a/src/pages/ReimbursementAccount/ValidationStep.js +++ b/src/pages/ReimbursementAccount/ValidationStep.js @@ -6,7 +6,8 @@ import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Button from '@components/Button'; -import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -144,7 +145,7 @@ function ValidationStep({reimbursementAccount, translate, onBackButtonPress, acc )} {!maxAttemptsReached && state === BankAccount.STATE.PENDING && ( -

{translate('validationStep.descriptionCTA')} - - - )} -
+ )} {isVerifying && ( diff --git a/src/pages/ReimbursementAccount/exampleCheckImage.js b/src/pages/ReimbursementAccount/exampleCheckImage.js deleted file mode 100644 index c3fdbdd3984c..000000000000 --- a/src/pages/ReimbursementAccount/exampleCheckImage.js +++ /dev/null @@ -1,14 +0,0 @@ -import exampleCheckImageEn from '@assets/images/example-check-image-en.png'; -import exampleCheckImageEs from '@assets/images/example-check-image-es.png'; -import CONST from '@src/CONST'; - -const images = { - [CONST.LOCALES.EN]: exampleCheckImageEn, - [CONST.LOCALES.ES]: exampleCheckImageEs, -}; - -function exampleCheckImage(languageKey = CONST.LOCALES.EN) { - return images[languageKey]; -} - -export default exampleCheckImage; diff --git a/src/pages/home/report/AnimatedEmptyStateBackground.js b/src/pages/home/report/AnimatedEmptyStateBackground.js index c003a3809130..0ff401a9d3f4 100644 --- a/src/pages/home/report/AnimatedEmptyStateBackground.js +++ b/src/pages/home/report/AnimatedEmptyStateBackground.js @@ -3,13 +3,14 @@ import Animated, {SensorType, useAnimatedSensor, useAnimatedStyle, useSharedValu import useWindowDimensions from '@hooks/useWindowDimensions'; import * as NumberUtils from '@libs/NumberUtils'; import useThemeIllustrations from '@styles/illustrations/useThemeIllustrations'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; const IMAGE_OFFSET_Y = 75; function AnimatedEmptyStateBackground() { + const StyleUtils = useStyleUtils(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const illustrations = useThemeIllustrations(); const IMAGE_OFFSET_X = windowWidth / 2; diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js index 051e559c34b6..33adfa4b35f9 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js @@ -12,9 +12,7 @@ import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useNetwork from '@hooks/useNetwork'; import compose from '@libs/compose'; -import getReportActionContextMenuStyles from '@styles/getReportActionContextMenuStyles'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; +import useStyleUtils from '@styles/useStyleUtils'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -51,11 +49,10 @@ const defaultProps = { ...GenericReportActionContextMenuDefaultProps, }; function BaseReportActionContextMenu(props) { - const theme = useTheme(); - const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const menuItemRefs = useRef({}); const [shouldKeepOpen, setShouldKeepOpen] = useState(false); - const wrapperStyle = getReportActionContextMenuStyles(styles, props.isMini, props.isSmallScreenWidth, theme); + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(props.isMini, props.isSmallScreenWidth); const {isOffline} = useNetwork(); const reportAction = useMemo(() => { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 4f35926c5957..832b3352b2e5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -7,6 +7,7 @@ import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactio import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import Clipboard from '@libs/Clipboard'; +import EmailUtils from '@libs/EmailUtils'; import * as Environment from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; @@ -243,10 +244,10 @@ export default [ successIcon: Expensicons.Checkmark, shouldShow: (type) => type === CONTEXT_MENU_TYPES.EMAIL, onPress: (closePopover, {selection}) => { - Clipboard.setString(selection.replace('mailto:', '')); + Clipboard.setString(EmailUtils.trimMailTo(selection)); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, - getDescription: (selection) => selection.replace('mailto:', ''), + getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection)), }, { isAnonymousAction: true, diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js index 37901db09954..f5a688e9d8ed 100644 --- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js +++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js @@ -7,8 +7,7 @@ import { defaultProps as GenericReportActionContextMenuDefaultProps, propTypes as genericReportActionContextMenuPropTypes, } from '@pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; -import useThemeStyles from '@styles/useThemeStyles'; +import useStyleUtils from '@styles/useStyleUtils'; import CONST from '@src/CONST'; const propTypes = { @@ -25,11 +24,11 @@ const defaultProps = { }; function MiniReportActionContextMenu(props) { - const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( = 0 ? Math.min(props.maxAmountOfPreviews, props.linkMetadata.length) : props.linkMetadata.length), (linkData) => { diff --git a/src/pages/home/report/ReactionList/HeaderReactionList.js b/src/pages/home/report/ReactionList/HeaderReactionList.js index 2d4e6bfbb35a..1b1751e32eef 100644 --- a/src/pages/home/report/ReactionList/HeaderReactionList.js +++ b/src/pages/home/report/ReactionList/HeaderReactionList.js @@ -6,8 +6,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import reactionPropTypes from './reactionPropTypes'; @@ -27,14 +26,14 @@ const defaultProps = { }; function HeaderReactionList(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( - + {props.emojiCodes.join('')} - {props.emojiCount} + {props.emojiCount} {`:${EmojiUtils.getLocalizedEmojiName(props.emojiName, props.preferredLocale)}:`} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index f7f7b92f1a56..f5a321f72799 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -26,8 +26,8 @@ import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; -import containerComposeStyles from '@styles/containerComposeStyles'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; @@ -103,6 +103,7 @@ function ComposerWithSuggestions({ }) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {preferredLocale} = useLocalize(); const isFocused = useIsFocused(); const navigation = useNavigation(); @@ -516,7 +517,7 @@ function ComposerWithSuggestions({ return ( <> - + ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); const [isHidden, setIsHidden] = useState(false); @@ -151,7 +152,10 @@ function ReportActionItem(props) { const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); const isReportActionLinked = props.linkedReportActionID === props.action.reportActionID; - const highlightedBackgroundColorIfNeeded = useMemo(() => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.highlightBG) : {}), [isReportActionLinked, theme]); + const highlightedBackgroundColorIfNeeded = useMemo( + () => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.highlightBG) : {}), + [StyleUtils, isReportActionLinked, theme.highlightBG], + ); const originalMessage = lodashGet(props.action, 'originalMessage', {}); // IOUDetails only exists when we are sending money @@ -686,7 +690,7 @@ function ReportActionItem(props) { draftMessage={props.draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} /> - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} pendingAction={props.draftMessage ? null : props.action.pendingAction} diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js index 4c7f14a21abc..01857e2f2f1c 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.js @@ -14,7 +14,7 @@ import compose from '@libs/compose'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; @@ -50,6 +50,7 @@ const defaultProps = { function ReportActionItemCreated(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); if (!ReportUtils.isChatReport(props.report)) { return null; } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 1ed5db385f7a..13d918aa25d7 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -27,8 +27,8 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import setShouldShowComposeInputKeyboardAware from '@libs/setShouldShowComposeInputKeyboardAware'; import reportPropTypes from '@pages/reportPropTypes'; -import containerComposeStyles from '@styles/containerComposeStyles'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; @@ -82,6 +82,7 @@ const isMobileSafari = Browser.isMobileSafari(); function ReportActionItemMessageEdit(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const reportScrollManager = useReportScrollManager(); const {translate, preferredLocale} = useLocalize(); const {isKeyboardShown} = useKeyboardState(); @@ -397,7 +398,7 @@ function ReportActionItemMessageEdit(props) {
- + { diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js index ad319b86e634..4a125d1d5633 100644 --- a/src/pages/home/report/ReportActionItemParentAction.js +++ b/src/pages/home/report/ReportActionItemParentAction.js @@ -9,7 +9,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import compose from '@libs/compose'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import reportPropTypes from '@pages/reportPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -47,6 +47,7 @@ const defaultProps = { function ReportActionItemParentAction(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const parentReportAction = props.parentReportActions[`${props.report.parentReportActionID}`]; // In case of transaction threads, we do not want to render the parent report action. diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index b13d57ad2976..03603f201edf 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -20,8 +20,8 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import reportPropTypes from '@pages/reportPropTypes'; import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -81,6 +81,7 @@ const showWorkspaceDetails = (reportID) => { function ReportActionItemSingle(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const actorAccountID = props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport ? props.iouReport.managerID : props.action.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); diff --git a/src/pages/home/report/withReportOrNotFound.tsx b/src/pages/home/report/withReportOrNotFound.tsx index 93d42ef6fd2b..cf2c0d5aca4b 100644 --- a/src/pages/home/report/withReportOrNotFound.tsx +++ b/src/pages/home/report/withReportOrNotFound.tsx @@ -39,7 +39,7 @@ export default function ( const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.report ?? {}).length || !props.report?.reportID); const shouldShowNotFoundPage = - !Object.entries(props.report ?? {}).length || !props.report?.reportID || !ReportUtils.canAccessReport(props.report, props.policies, props.betas, {}); + !Object.entries(props.report ?? {}).length || !props.report?.reportID || !ReportUtils.canAccessReport(props.report, props.policies, props.betas); // If the content was shown but it's not anymore that means the report was deleted and we are probably navigating out of this screen. // Return null for this case to avoid rendering FullScreenLoadingIndicator or NotFoundPage when animating transition. diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 2aba742f157f..e9a4b75732e0 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -19,8 +19,8 @@ import onyxSubscribe from '@libs/onyxSubscribe'; import SidebarUtils from '@libs/SidebarUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as App from '@userActions/App'; @@ -54,6 +54,7 @@ const propTypes = { function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen}) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const modal = useRef({}); const {translate, updateLocale} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 088eb5c0092a..baae0fda33e3 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -9,7 +9,6 @@ import withCurrentReportID from '@components/withCurrentReportID'; import withNavigationFocus from '@components/withNavigationFocus'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; -import * as SessionUtils from '@libs/SessionUtils'; import SidebarUtils from '@libs/SidebarUtils'; import reportPropTypes from '@pages/reportPropTypes'; import useThemeStyles from '@styles/useThemeStyles'; @@ -41,7 +40,7 @@ const propTypes = { ), /** Whether the reports are loading. When false it means they are ready to be used. */ - isLoadingReportData: PropTypes.bool, + isLoadingApp: PropTypes.bool, /** The chat priority mode */ priorityMode: PropTypes.string, @@ -57,18 +56,18 @@ const propTypes = { const defaultProps = { chatReports: {}, allReportActions: {}, - isLoadingReportData: true, + isLoadingApp: true, priorityMode: CONST.PRIORITY_MODE.DEFAULT, betas: [], policies: {}, }; -function SidebarLinksData({isFocused, allReportActions, betas, chatReports, currentReportID, insets, isLoadingReportData, onLinkClick, policies, priorityMode}) { +function SidebarLinksData({isFocused, allReportActions, betas, chatReports, currentReportID, insets, isLoadingApp, onLinkClick, policies, priorityMode}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const reportIDsRef = useRef(null); - const isLoading = SessionUtils.didUserLogInDuringSession() && isLoadingReportData; + const isLoading = isLoadingApp; const optionListItems = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions); if (deepEqual(reportIDsRef.current, reportIDs)) { @@ -201,8 +200,8 @@ export default compose( selector: chatReportSelector, initialValue: {}, }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js index f0aea2301383..fa1c17578126 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js @@ -16,8 +16,8 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -74,6 +74,7 @@ const defaultProps = { function BaseValidateCodeForm(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [formError, setFormError] = useState({}); const [validateCode, setValidateCode] = useState(''); const loginData = props.loginList[props.contactMethod]; @@ -209,7 +210,7 @@ function BaseValidateCodeForm(props) { role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('validateCodeForm.magicCodeNotReceived')} > - {props.translate('validateCodeForm.magicCodeNotReceived')} + {props.translate('validateCodeForm.magicCodeNotReceived')} {props.hasMagicCodeBeenSent && ( onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID), - iconFill: isMethodActive ? StyleUtils.getIconFillColor(theme, CONST.BUTTON_STATES.PRESSED) : null, - wrapperStyle: isMethodActive ? [StyleUtils.getButtonBackgroundColorStyle(theme, CONST.BUTTON_STATES.PRESSED)] : null, + iconFill: isMethodActive ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null, + wrapperStyle: isMethodActive ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null, disabled: paymentMethod.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, }; }); return combinedPaymentMethods; - }, [shouldShowAssignedCards, fundList, bankAccountList, filterType, isOffline, cardList, translate, actionPaymentMethodType, activePaymentMethodID, onPress, styles, theme]); + }, [shouldShowAssignedCards, fundList, bankAccountList, styles, filterType, isOffline, cardList, translate, actionPaymentMethodType, activePaymentMethodID, StyleUtils, onPress]); /** * Render placeholder when there are no payments methods diff --git a/src/pages/signin/SignInHeroCopy.js b/src/pages/signin/SignInHeroCopy.js index 7c770169df82..75c53c6b2344 100644 --- a/src/pages/signin/SignInHeroCopy.js +++ b/src/pages/signin/SignInHeroCopy.js @@ -5,7 +5,7 @@ import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import compose from '@libs/compose'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; @@ -27,6 +27,7 @@ const defaultProps = { function SignInHeroCopy(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( [ function Footer(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const isVertical = props.shouldShowSmallScreen; const imageDirection = isVertical ? styles.flexRow : styles.flexColumn; const imageStyle = isVertical ? styles.pr0 : styles.alignSelfCenter; diff --git a/src/pages/signin/SignInPageLayout/SignInPageContent.js b/src/pages/signin/SignInPageLayout/SignInPageContent.js index d09cee9aa1c2..fd48ebecf066 100755 --- a/src/pages/signin/SignInPageLayout/SignInPageContent.js +++ b/src/pages/signin/SignInPageLayout/SignInPageContent.js @@ -10,7 +10,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import SignInHeroImage from '@pages/signin/SignInHeroImage'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; @@ -41,6 +41,7 @@ const propTypes = { function SignInPageContent(props) { const {isSmallScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js index b6c82fc843cd..667f873c572e 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.js @@ -8,8 +8,8 @@ import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import SignInPageHero from '@pages/signin/SignInPageHero'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import BackgroundImage from './BackgroundImage'; @@ -60,6 +60,7 @@ const defaultProps = { function SignInPageLayout(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const scrollViewRef = useRef(); const prevPreferredLocale = usePrevious(props.preferredLocale); let containerStyles = [styles.flex1, styles.signInPageInner]; diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 7c48d557cd16..3104042bf33b 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -20,8 +20,8 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; import Terms from '@pages/signin/Terms'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -84,6 +84,7 @@ const defaultProps = { function BaseValidateCodeForm(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [formError, setFormError] = useState({}); const [validateCode, setValidateCode] = useState(props.credentials.validateCode || ''); const [twoFactorAuthCode, setTwoFactorAuthCode] = useState(''); @@ -378,7 +379,7 @@ function BaseValidateCodeForm(props) { role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('validateCodeForm.magicCodeNotReceived')} > - + {hasError ? props.translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : props.translate('validateCodeForm.magicCodeNotReceived')} diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index b5973e35e07c..b1b79f1c3f66 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -26,6 +26,7 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SearchInputManager from './SearchInputManager'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -126,6 +127,7 @@ class WorkspaceInviteMessagePage extends React.Component { Keyboard.dismiss(); Policy.addMembersToWorkspace(this.props.invitedEmailsToAccountIDsDraft, this.state.welcomeNote, this.props.route.params.policyID); Policy.setWorkspaceInviteMembersDraft(this.props.route.params.policyID, {}); + SearchInputManager.searchInput = ''; // Pop the invite message page before navigating to the members page. Navigation.goBack(ROUTES.HOME); Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(this.props.route.params.policyID)); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index b18c234ea44d..3528224f39b9 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -77,9 +77,6 @@ function WorkspaceInvitePage(props) { }; useEffect(() => { - if (!SearchInputManager.searchInput) { - return; - } setSearchTerm(SearchInputManager.searchInput); }, []); diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index d5cdbcfc69d8..59993087c44c 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -90,9 +90,6 @@ function WorkspaceMembersPage(props) { const isFocusedScreen = useIsFocused(); useEffect(() => { - if (!SearchInputManager.searchInput) { - return; - } setSearchValue(SearchInputManager.searchInput); }, [isFocusedScreen]); diff --git a/src/stories/Composer.stories.js b/src/stories/Composer.stories.js index 2db1011d1b3a..799012e1c051 100644 --- a/src/stories/Composer.stories.js +++ b/src/stories/Composer.stories.js @@ -6,8 +6,8 @@ import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import withNavigationFallback from '@components/withNavigationFallback'; import styles from '@styles/styles'; -import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; +import useStyleUtils from '@styles/useStyleUtils'; import CONST from '@src/CONST'; const ComposerWithNavigation = withNavigationFallback(Composer); @@ -25,6 +25,7 @@ const story = { const parser = new ExpensiMark(); function Default(args) { + const StyleUtils = useStyleUtils(); const [pastedFile, setPastedFile] = useState(null); const [comment, setComment] = useState(args.defaultValue); const renderedHTML = parser.replace(comment); diff --git a/src/styles/getPopoverWithMeasuredContentStyles.ts b/src/styles/PopoverWithMeasuredContentStyleUtils.ts similarity index 89% rename from src/styles/getPopoverWithMeasuredContentStyles.ts rename to src/styles/PopoverWithMeasuredContentStyleUtils.ts index 4d4e868f823b..94a0fb77d8bb 100644 --- a/src/styles/getPopoverWithMeasuredContentStyles.ts +++ b/src/styles/PopoverWithMeasuredContentStyleUtils.ts @@ -1,4 +1,4 @@ -import roundToNearestMultipleOfFour from './roundToNearestMultipleOfFour'; +import roundToNearestMultipleOfFour from './utils/roundToNearestMultipleOfFour'; import variables from './variables'; /** @@ -50,4 +50,6 @@ function computeVerticalShift(anchorTopEdge: number, menuHeight: number, windowH return 0; } -export {computeHorizontalShift, computeVerticalShift}; +const PopoverWithMeasuredContentStyleUtils = {computeHorizontalShift, computeVerticalShift}; + +export default PopoverWithMeasuredContentStyleUtils; diff --git a/src/styles/ThemeStylesContext.ts b/src/styles/ThemeStylesContext.ts index 1c81ab3b39a5..73f044f2dcbd 100644 --- a/src/styles/ThemeStylesContext.ts +++ b/src/styles/ThemeStylesContext.ts @@ -1,6 +1,15 @@ import React from 'react'; -import styles from './styles'; +import {defaultStyles} from './styles'; +import type {ThemeStyles} from './styles'; +import {DefaultStyleUtils} from './utils'; +import type {StyleUtilsType} from './utils'; -const ThemeStylesContext = React.createContext(styles); +type ThemeStylesContextType = { + styles: ThemeStyles; + StyleUtils: StyleUtilsType; +}; + +const ThemeStylesContext = React.createContext({styles: defaultStyles, StyleUtils: DefaultStyleUtils}); export default ThemeStylesContext; +export {type ThemeStylesContextType}; diff --git a/src/styles/ThemeStylesProvider.tsx b/src/styles/ThemeStylesProvider.tsx index 1a60d61c4ea5..766cb00bd09b 100644 --- a/src/styles/ThemeStylesProvider.tsx +++ b/src/styles/ThemeStylesProvider.tsx @@ -1,16 +1,19 @@ import React, {useMemo} from 'react'; -import {stylesGenerator} from './styles'; +import stylesGenerator from './styles'; import useTheme from './themes/useTheme'; import ThemeStylesContext from './ThemeStylesContext'; +import createStyleUtils from './utils'; type ThemeStylesProviderProps = React.PropsWithChildren; function ThemeStylesProvider({children}: ThemeStylesProviderProps) { const theme = useTheme(); - const themeStyles = useMemo(() => stylesGenerator(theme), [theme]); + const styles = useMemo(() => stylesGenerator(theme), [theme]); + const StyleUtils = useMemo(() => createStyleUtils(theme, styles), [theme, styles]); + const contextValue = useMemo(() => ({styles, StyleUtils}), [styles, StyleUtils]); - return {children}; + return {children}; } ThemeStylesProvider.displayName = 'ThemeStylesProvider'; diff --git a/src/styles/colors.ts b/src/styles/colors.ts index fbe694e051ee..35d93f060c86 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -19,27 +19,26 @@ const colors: Record = { transparent: 'transparent', // Dark Mode Theme Colors - darkAppBackground: '#061B09', - darkHighlightBackground: '#07271F', - darkBorders: '#1A3D32', - darkIcons: '#8B9C8F', - darkSupportingText: '#AFBBB0', - darkPrimaryText: '#E7ECE9', - darkDefaultButton: '#184E3D', - darkDefaultButtonHover: '#2C6755', - darkDefaultButtonPressed: '#467164', + productDark100: '#061B09', + productDark200: '#072419', + productDark300: '#0A2E25', + productDark400: '#1A3D32', + productDark500: '#224F41', + productDark600: '#2A604F', + productDark700: '#8B9C8F', + productDark800: '#AFBBB0', + productDark900: '#E7ECE9', // Light Mode Theme Colors - lightAppBackground: '#FCFBF9', - lightHighlightBackground: '#F8F4F0', - lightBorders: '#EBE6DF', - lightBordersLighter: '#2B5548', - lightIcons: '#A2A9A3', - lightSupportingText: '#76847E', - lightPrimaryText: '#002E22', - lightDefaultButton: '#EEEBE7', - lightDefaultButtonHover: '#E3DFD9', - lightDefaultButtonPressed: '#D2CCC3', + productLight100: '#FCFBF9', + productLight200: '#F8F4F0', + productLight300: '#F2EDE7', + productLight400: '#E6E1DA', + productLight500: '#D8D1C7', + productLight600: '#C7BFB3', + productLight700: '#A2A9A3', + productLight800: '#76847E', + productLight900: '#002E22', // Brand Colors from Figma blue100: '#B0D9FF', diff --git a/src/styles/getModalStyles.ts b/src/styles/getModalStyles.ts deleted file mode 100644 index b11b350e4e9d..000000000000 --- a/src/styles/getModalStyles.ts +++ /dev/null @@ -1,271 +0,0 @@ -import {ViewStyle} from 'react-native'; -import {ModalProps} from 'react-native-modal'; -import {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; -import {type ThemeStyles} from './styles'; -import {type ThemeColors} from './themes/types'; -import variables from './variables'; - -function getCenteredModalStyles(styles: ThemeStyles, windowWidth: number, isSmallScreenWidth: boolean, isFullScreenWhenSmall = false): ViewStyle { - const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, isFullScreenWhenSmall); - - return { - borderWidth: modalStyles.borderWidth, - width: isSmallScreenWidth ? '100%' : windowWidth - modalStyles.marginHorizontal * 2, - }; -} - -type ModalType = ValueOf; - -type WindowDimensions = { - windowWidth: number; - windowHeight: number; - isSmallScreenWidth: boolean; -}; - -type GetModalStyles = { - modalStyle: ViewStyle; - modalContainerStyle: ViewStyle; - swipeDirection: ModalProps['swipeDirection']; - animationIn: ModalProps['animationIn']; - animationOut: ModalProps['animationOut']; - hideBackdrop: boolean; - shouldAddTopSafeAreaMargin: boolean; - shouldAddBottomSafeAreaMargin: boolean; - shouldAddBottomSafeAreaPadding: boolean; - shouldAddTopSafeAreaPadding: boolean; -}; - -export default function getModalStyles( - theme: ThemeColors, - styles: ThemeStyles, - type: ModalType | undefined, - windowDimensions: WindowDimensions, - popoverAnchorPosition: ViewStyle = {}, - innerContainerStyle: ViewStyle = {}, - outerStyle: ViewStyle = {}, -): GetModalStyles { - const {isSmallScreenWidth, windowWidth} = windowDimensions; - - let modalStyle: GetModalStyles['modalStyle'] = { - margin: 0, - ...outerStyle, - }; - - let modalContainerStyle: GetModalStyles['modalContainerStyle']; - let swipeDirection: GetModalStyles['swipeDirection']; - let animationIn: GetModalStyles['animationIn']; - let animationOut: GetModalStyles['animationOut']; - let hideBackdrop = false; - let shouldAddBottomSafeAreaMargin = false; - let shouldAddTopSafeAreaMargin = false; - let shouldAddBottomSafeAreaPadding = false; - let shouldAddTopSafeAreaPadding = false; - - switch (type) { - case CONST.MODAL.MODAL_TYPE.CONFIRM: - // A confirm modal is one that has a visible backdrop - // and can be dismissed by clicking outside of the modal. - modalStyle = { - ...modalStyle, - ...{ - alignItems: 'center', - }, - }; - modalContainerStyle = { - boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', - borderRadius: 12, - overflow: 'hidden', - width: variables.sideBarWidth, - }; - - // setting this to undefined we effectively disable the - // ability to swipe our modal - swipeDirection = undefined; - animationIn = 'fadeIn'; - animationOut = 'fadeOut'; - break; - case CONST.MODAL.MODAL_TYPE.CENTERED: - // A centered modal is one that has a visible backdrop - // and can be dismissed by clicking outside of the modal. - // This modal should take up the entire visible area when - // viewed on a smaller device (e.g. mobile or mobile web). - modalStyle = { - ...modalStyle, - ...{ - alignItems: 'center', - }, - }; - modalContainerStyle = { - boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', - flex: 1, - marginTop: isSmallScreenWidth ? 0 : 20, - marginBottom: isSmallScreenWidth ? 0 : 20, - borderRadius: isSmallScreenWidth ? 0 : 12, - overflow: 'hidden', - ...getCenteredModalStyles(styles, windowWidth, isSmallScreenWidth), - }; - - // Allow this modal to be dismissed with a swipe down or swipe right - swipeDirection = ['down', 'right']; - animationIn = isSmallScreenWidth ? 'slideInRight' : 'fadeIn'; - animationOut = isSmallScreenWidth ? 'slideOutRight' : 'fadeOut'; - shouldAddTopSafeAreaMargin = !isSmallScreenWidth; - shouldAddBottomSafeAreaMargin = !isSmallScreenWidth; - shouldAddTopSafeAreaPadding = isSmallScreenWidth; - shouldAddBottomSafeAreaPadding = false; - break; - case CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE: - // A centered modal that cannot be dismissed with a swipe. - modalStyle = { - ...modalStyle, - ...{ - alignItems: 'center', - }, - }; - modalContainerStyle = { - boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', - flex: 1, - marginTop: isSmallScreenWidth ? 0 : 20, - marginBottom: isSmallScreenWidth ? 0 : 20, - borderRadius: isSmallScreenWidth ? 0 : 12, - overflow: 'hidden', - ...getCenteredModalStyles(styles, windowWidth, isSmallScreenWidth, true), - }; - swipeDirection = undefined; - animationIn = isSmallScreenWidth ? 'slideInRight' : 'fadeIn'; - animationOut = isSmallScreenWidth ? 'slideOutRight' : 'fadeOut'; - shouldAddTopSafeAreaMargin = !isSmallScreenWidth; - shouldAddBottomSafeAreaMargin = !isSmallScreenWidth; - shouldAddTopSafeAreaPadding = isSmallScreenWidth; - shouldAddBottomSafeAreaPadding = false; - break; - case CONST.MODAL.MODAL_TYPE.CENTERED_SMALL: - // A centered modal that takes up the minimum possible screen space on all devices - modalStyle = { - ...modalStyle, - ...{ - alignItems: 'center', - }, - }; - modalContainerStyle = { - boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', - borderRadius: 12, - borderWidth: 0, - }; - - // Allow this modal to be dismissed with a swipe down or swipe right - swipeDirection = ['down', 'right']; - animationIn = 'fadeIn'; - animationOut = 'fadeOut'; - shouldAddTopSafeAreaMargin = false; - shouldAddBottomSafeAreaMargin = false; - shouldAddTopSafeAreaPadding = false; - shouldAddBottomSafeAreaPadding = false; - break; - case CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED: - modalStyle = { - ...modalStyle, - ...{ - alignItems: 'center', - justifyContent: 'flex-end', - }, - }; - modalContainerStyle = { - width: '100%', - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - paddingTop: 12, - justifyContent: 'center', - overflow: 'hidden', - }; - - shouldAddBottomSafeAreaPadding = true; - swipeDirection = undefined; - animationIn = 'slideInUp'; - animationOut = 'slideOutDown'; - break; - case CONST.MODAL.MODAL_TYPE.POPOVER: - modalStyle = { - ...modalStyle, - ...popoverAnchorPosition, - ...{ - position: 'absolute', - alignItems: 'center', - justifyContent: 'flex-end', - }, - }; - modalContainerStyle = { - borderRadius: 12, - borderWidth: 1, - borderColor: theme.border, - justifyContent: 'center', - overflow: 'hidden', - boxShadow: variables.popoverMenuShadow, - }; - - hideBackdrop = true; - swipeDirection = undefined; - animationIn = 'fadeIn'; - animationOut = 'fadeOut'; - break; - case CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED: - modalStyle = { - ...modalStyle, - ...{ - marginLeft: isSmallScreenWidth ? 0 : windowWidth - variables.sideBarWidth, - width: isSmallScreenWidth ? '100%' : variables.sideBarWidth, - flexDirection: 'row', - justifyContent: 'flex-end', - }, - }; - modalContainerStyle = { - width: isSmallScreenWidth ? '100%' : variables.sideBarWidth, - height: '100%', - overflow: 'hidden', - }; - - animationIn = { - from: { - translateX: isSmallScreenWidth ? windowWidth : variables.sideBarWidth, - }, - to: { - translateX: 0, - }, - }; - animationOut = { - from: { - translateX: 0, - }, - to: { - translateX: isSmallScreenWidth ? windowWidth : variables.sideBarWidth, - }, - }; - hideBackdrop = true; - swipeDirection = undefined; - shouldAddBottomSafeAreaPadding = true; - shouldAddTopSafeAreaPadding = true; - break; - default: - modalStyle = {}; - modalContainerStyle = {}; - swipeDirection = 'down'; - animationIn = 'slideInUp'; - animationOut = 'slideOutDown'; - } - - modalContainerStyle = {...modalContainerStyle, ...innerContainerStyle}; - - return { - modalStyle, - modalContainerStyle, - swipeDirection, - animationIn, - animationOut, - hideBackdrop, - shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaMargin, - shouldAddBottomSafeAreaPadding, - shouldAddTopSafeAreaPadding, - }; -} diff --git a/src/styles/getTooltipStyles.ts b/src/styles/getTooltipStyles.ts deleted file mode 100644 index 1adfa1969ab9..000000000000 --- a/src/styles/getTooltipStyles.ts +++ /dev/null @@ -1,301 +0,0 @@ -import {TextStyle, View, ViewStyle} from 'react-native'; -import fontFamily from './fontFamily'; -import roundToNearestMultipleOfFour from './roundToNearestMultipleOfFour'; -import {type ThemeStyles} from './styles'; -import {type ThemeColors} from './themes/types'; -import positioning from './utilities/positioning'; -import spacing from './utilities/spacing'; -import variables from './variables'; - -/** This defines the proximity with the edge of the window in which tooltips should not be displayed. - * If a tooltip is too close to the edge of the screen, we'll shift it towards the center. */ -const GUTTER_WIDTH = variables.gutterWidth; - -/** The height of a tooltip pointer */ -const POINTER_HEIGHT = 4; - -/** The width of a tooltip pointer */ -const POINTER_WIDTH = 12; - -/** - * Compute the amount the tooltip needs to be horizontally shifted in order to keep it from displaying in the gutters. - * - * @param windowWidth - The width of the window. - * @param xOffset - The distance between the left edge of the window - * and the left edge of the wrapped component. - * @param componentWidth - The width of the wrapped component. - * @param tooltipWidth - The width of the tooltip itself. - * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. - * A positive value shifts it to the right, - * and a negative value shifts it to the left. - */ -function computeHorizontalShift(windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number): number { - // First find the left and right edges of the tooltip (by default, it is centered on the component). - const componentCenter = xOffset + componentWidth / 2 + manualShiftHorizontal; - const tooltipLeftEdge = componentCenter - tooltipWidth / 2; - const tooltipRightEdge = componentCenter + tooltipWidth / 2; - - if (tooltipLeftEdge < GUTTER_WIDTH) { - // Tooltip is in left gutter, shift right by a multiple of four. - return roundToNearestMultipleOfFour(GUTTER_WIDTH - tooltipLeftEdge); - } - - if (tooltipRightEdge > windowWidth - GUTTER_WIDTH) { - // Tooltip is in right gutter, shift left by a multiple of four. - return roundToNearestMultipleOfFour(windowWidth - GUTTER_WIDTH - tooltipRightEdge); - } - - // Tooltip is not in the gutter, so no need to shift it horizontally - return 0; -} - -/** - * Determines if there is an overlapping element at the top of a given coordinate. - * (targetCenterX, y) - * | - * v - * _ _ _ _ _ - * | | - * | | - * | | - * | | - * |_ _ _ _ _| - * - * @param tooltip - The reference to the tooltip's root element - * @param xOffset - The distance between the left edge of the window - * and the left edge of the wrapped component. - * @param yOffset - The distance between the top edge of the window - * and the top edge of the wrapped component. - * @param tooltipTargetWidth - The width of the tooltip's target - * @param tooltipTargetHeight - The height of the tooltip's target - */ -function isOverlappingAtTop(tooltip: View | HTMLDivElement, xOffset: number, yOffset: number, tooltipTargetWidth: number, tooltipTargetHeight: number) { - if (typeof document.elementFromPoint !== 'function') { - return false; - } - - // Use the x center position of the target to prevent wrong element returned by elementFromPoint - // in case the target has a border radius or is a multiline text. - const targetCenterX = xOffset + tooltipTargetWidth / 2; - const elementAtTargetCenterX = document.elementFromPoint(targetCenterX, yOffset); - - // Ensure it's not the already rendered element of this very tooltip, so the tooltip doesn't try to "avoid" itself - if (!elementAtTargetCenterX || ('contains' in tooltip && tooltip.contains(elementAtTargetCenterX))) { - return false; - } - - const rectAtTargetCenterX = elementAtTargetCenterX.getBoundingClientRect(); - - // Ensure it's not overlapping with another element by checking if the yOffset is greater than the top of the element - // and less than the bottom of the element. Also ensure the tooltip target is not completely inside the elementAtTargetCenterX by vertical direction - const isOverlappingAtTargetCenterX = yOffset > rectAtTargetCenterX.top && yOffset < rectAtTargetCenterX.bottom && yOffset + tooltipTargetHeight > rectAtTargetCenterX.bottom; - - return isOverlappingAtTargetCenterX; -} - -type TooltipStyles = { - animationStyle: ViewStyle; - rootWrapperStyle: ViewStyle; - textStyle: TextStyle; - pointerWrapperStyle: ViewStyle; - pointerStyle: ViewStyle; -}; - -type TooltipParams = { - tooltip: View | HTMLDivElement; - currentSize: number; - windowWidth: number; - xOffset: number; - yOffset: number; - tooltipTargetWidth: number; - tooltipTargetHeight: number; - maxWidth: number; - tooltipContentWidth: number; - tooltipWrapperHeight: number; - theme: ThemeColors; - styles: ThemeStyles; - manualShiftHorizontal?: number; - manualShiftVertical?: number; -}; - -/** - * Generate styles for the tooltip component. - * - * @param tooltip - The reference to the tooltip's root element - * @param currentSize - The current size of the tooltip used in the scaling animation. - * @param windowWidth - The width of the window. - * @param xOffset - The distance between the left edge of the window - * and the left edge of the wrapped component. - * @param yOffset - The distance between the top edge of the window - * and the top edge of the wrapped component. - * @param tooltipTargetWidth - The width of the tooltip's target - * @param tooltipTargetHeight - The height of the tooltip's target - * @param maxWidth - The tooltip's max width. - * @param tooltipContentWidth - The tooltip's inner content measured width. - * @param tooltipWrapperHeight - The tooltip's wrapper measured height. - * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. - * A positive value shifts it to the right, - * and a negative value shifts it to the left. - * @param [manualShiftVertical] - Any additional amount to manually shift the tooltip up or down. - * A positive value shifts it down, and a negative value shifts it up. - */ -export default function getTooltipStyles({ - tooltip, - currentSize, - windowWidth, - xOffset, - yOffset, - tooltipTargetWidth, - tooltipTargetHeight, - maxWidth, - tooltipContentWidth, - tooltipWrapperHeight, - theme, - styles, - manualShiftHorizontal = 0, - manualShiftVertical = 0, -}: TooltipParams): TooltipStyles { - const tooltipVerticalPadding = spacing.pv1; - - // We calculate tooltip width based on the tooltip's content width - // so the tooltip wrapper is just big enough to fit content and prevent white space. - // NOTE: Add 1 to the tooltipWidth to prevent truncated text in Safari - const tooltipWidth = tooltipContentWidth && tooltipContentWidth + spacing.ph2.paddingHorizontal * 2 + 1; - const tooltipHeight = tooltipWrapperHeight; - - const isTooltipSizeReady = tooltipWidth !== undefined && tooltipHeight !== undefined; - - // Set the scale to 1 to be able to measure the tooltip size correctly when it's not ready yet. - let scale = 1; - let shouldShowBelow = false; - let horizontalShift = 0; - let horizontalShiftPointer = 0; - let rootWrapperTop = 0; - let rootWrapperLeft = 0; - let pointerWrapperTop = 0; - let pointerWrapperLeft = 0; - let pointerAdditionalStyle = {}; - - if (isTooltipSizeReady) { - // Determine if the tooltip should display below the wrapped component. - // If either a tooltip will try to render within GUTTER_WIDTH logical pixels of the top of the screen, - // Or the wrapped component is overlapping at top-center with another element - // we'll display it beneath its wrapped component rather than above it as usual. - shouldShowBelow = yOffset - tooltipHeight < GUTTER_WIDTH || isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight); - - // When the tooltip size is ready, we can start animating the scale. - scale = currentSize; - - // Determine if we need to shift the tooltip horizontally to prevent it - // from displaying too near to the edge of the screen. - horizontalShift = computeHorizontalShift(windowWidth, xOffset, tooltipTargetWidth, tooltipWidth, manualShiftHorizontal); - - // Determine if we need to shift the pointer horizontally to prevent it from being too near to the edge of the tooltip - // We shift it to the right a bit if the tooltip is positioned on the extreme left - // and shift it to left a bit if the tooltip is positioned on the extreme right. - horizontalShiftPointer = - horizontalShift > 0 - ? Math.max(-horizontalShift, -(tooltipWidth / 2) + POINTER_WIDTH / 2 + variables.componentBorderRadiusSmall) - : Math.min(-horizontalShift, tooltipWidth / 2 - POINTER_WIDTH / 2 - variables.componentBorderRadiusSmall); - - // Because it uses fixed positioning, the top-left corner of the tooltip is aligned - // with the top-left corner of the window by default. - // we will use yOffset to position the tooltip relative to the Wrapped Component - // So we need to shift the tooltip vertically and horizontally to position it correctly. - // - // First, we'll position it vertically. - // To shift the tooltip down, we'll give `top` a positive value. - // To shift the tooltip up, we'll give `top` a negative value. - rootWrapperTop = shouldShowBelow - ? // We need to shift the tooltip down below the component. So shift the tooltip down (+) by... - yOffset + tooltipTargetHeight + POINTER_HEIGHT + manualShiftVertical - : // We need to shift the tooltip up above the component. So shift the tooltip up (-) by... - yOffset - (tooltipHeight + POINTER_HEIGHT) + manualShiftVertical; - - // Next, we'll position it horizontally. - // we will use xOffset to position the tooltip relative to the Wrapped Component - // To shift the tooltip right, we'll give `left` a positive value. - // To shift the tooltip left, we'll give `left` a negative value. - // - // So we'll: - // 1) Shift the tooltip right (+) to the center of the component, - // so the left edge lines up with the component center. - // 2) Shift it left (-) to by half the tooltip's width, - // so the tooltip's center lines up with the center of the wrapped component. - // 3) Add the horizontal shift (left or right) computed above to keep it out of the gutters. - // 4) Lastly, add the manual horizontal shift passed in as a parameter. - rootWrapperLeft = xOffset + (tooltipTargetWidth / 2 - tooltipWidth / 2) + horizontalShift + manualShiftHorizontal; - - // By default, the pointer's top-left will align with the top-left of the tooltip wrapper. - // - // To align it vertically, we'll: - // If the pointer should be below the tooltip wrapper, shift the pointer down (+) by the tooltip height, - // so that the top of the pointer lines up with the bottom of the tooltip - // - // OR if the pointer should be above the tooltip wrapper, then the pointer up (-) by the pointer's height - // so that the bottom of the pointer lines up with the top of the tooltip - pointerWrapperTop = shouldShowBelow ? -POINTER_HEIGHT : tooltipHeight; - - // To align it horizontally, we'll: - // 1) Shift the pointer to the right (+) by the half the tooltipWidth's width, - // so the left edge of the pointer lines up with the tooltipWidth's center. - // 2) To the left (-) by half the pointer's width, - // so the pointer's center lines up with the tooltipWidth's center. - // 3) Remove the wrapper's horizontalShift to maintain the pointer - // at the center of the hovered component. - pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2); - - pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {}; - } - - return { - animationStyle: { - // remember Transform causes a new Local cordinate system - // https://drafts.csswg.org/css-transforms-1/#transform-rendering - // so Position fixed children will be relative to this new Local cordinate system - transform: [{scale}], - }, - rootWrapperStyle: { - ...positioning.pFixed, - backgroundColor: theme.heading, - borderRadius: variables.componentBorderRadiusSmall, - ...tooltipVerticalPadding, - ...spacing.ph2, - zIndex: variables.tooltipzIndex, - width: tooltipWidth, - maxWidth, - top: rootWrapperTop, - left: rootWrapperLeft, - - // We are adding this to prevent the tooltip text from being selected and copied on CTRL + A. - ...styles.userSelectNone, - ...styles.pointerEventsNone, - }, - textStyle: { - color: theme.textReversed, - fontFamily: fontFamily.EXP_NEUE, - fontSize: variables.fontSizeSmall, - overflow: 'hidden', - lineHeight: variables.lineHeightSmall, - textAlign: 'center', - }, - pointerWrapperStyle: { - ...positioning.pFixed, - top: pointerWrapperTop, - left: pointerWrapperLeft, - }, - pointerStyle: { - width: 0, - height: 0, - backgroundColor: theme.transparent, - borderStyle: 'solid', - borderLeftWidth: POINTER_WIDTH / 2, - borderRightWidth: POINTER_WIDTH / 2, - borderTopWidth: POINTER_HEIGHT, - borderLeftColor: theme.transparent, - borderRightColor: theme.transparent, - borderTopColor: theme.heading, - ...pointerAdditionalStyle, - }, - }; -} diff --git a/src/styles/illustrations/dark.ts b/src/styles/illustrations/dark.ts index 6bee85cdedbd..ca52a24a4e4e 100644 --- a/src/styles/illustrations/dark.ts +++ b/src/styles/illustrations/dark.ts @@ -1,8 +1,12 @@ -import EmptyStateBackgroundImage from '@assets/images/empty-state_background-fade-dark.png'; +import EmptyStateBackgroundImage from '@assets/images/themeDependent/empty-state_background-fade-dark.png'; +import ExampleCheckEN from '@assets/images/themeDependent/example-check-image-dark-en.png'; +import ExampleCheckES from '@assets/images/themeDependent/example-check-image-dark-es.png'; import {Illustrations} from './types'; const illustrations = { EmptyStateBackgroundImage, + ExampleCheckEN, + ExampleCheckES, } satisfies Illustrations; export default illustrations; diff --git a/src/styles/illustrations/light.ts b/src/styles/illustrations/light.ts index 376a6d332f79..a953a312327f 100644 --- a/src/styles/illustrations/light.ts +++ b/src/styles/illustrations/light.ts @@ -1,8 +1,12 @@ -import EmptyStateBackgroundImage from '@assets/images/empty-state_background-fade-light.png'; +import EmptyStateBackgroundImage from '@assets/images/themeDependent/empty-state_background-fade-light.png'; +import ExampleCheckEN from '@assets/images/themeDependent/example-check-image-light-en.png'; +import ExampleCheckES from '@assets/images/themeDependent/example-check-image-light-es.png'; import {Illustrations} from './types'; const illustrations = { EmptyStateBackgroundImage, + ExampleCheckEN, + ExampleCheckES, } satisfies Illustrations; export default illustrations; diff --git a/src/styles/illustrations/types.ts b/src/styles/illustrations/types.ts index aebf01428994..bfb1db4a19c1 100644 --- a/src/styles/illustrations/types.ts +++ b/src/styles/illustrations/types.ts @@ -2,6 +2,8 @@ import {ImageSourcePropType} from 'react-native'; type Illustrations = { EmptyStateBackgroundImage: ImageSourcePropType; + ExampleCheckES: ImageSourcePropType; + ExampleCheckEN: ImageSourcePropType; }; // eslint-disable-next-line import/prefer-default-export diff --git a/src/styles/styles.ts b/src/styles/styles.ts index b88119beae74..76674b26c65c 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -20,7 +20,7 @@ import overflowXHidden from './overflowXHidden'; import pointerEventsAuto from './pointerEventsAuto'; import pointerEventsBoxNone from './pointerEventsBoxNone'; import pointerEventsNone from './pointerEventsNone'; -import defaultTheme from './themes/default'; +import {defaultTheme} from './themes/themes'; import {type ThemeColors} from './themes/types'; import borders from './utilities/borders'; import cursor from './utilities/cursor'; @@ -1339,7 +1339,7 @@ const styles = (theme: ThemeColors) => // The bottom of the floating action button should align with the bottom of the compose box. // The value should be equal to the height + marginBottom + marginTop of chatItemComposeSecondaryRow - bottom: 25, + bottom: variables.fabBottom, }, floatingActionButton: { @@ -1398,7 +1398,9 @@ const styles = (theme: ThemeColors) => createMenuPositionSidebar: (windowHeight: number) => ({ horizontal: 18, - vertical: windowHeight - 75, + // Menu should be displayed 12px above the floating action button. + // To achieve that sidebar must be moved by: distance from the bottom of the sidebar to the fab (variables.fabBottom) + fab height (variables.componentSizeLarge) + distance above the fab (12px) + vertical: windowHeight - (variables.fabBottom + variables.componentSizeLarge + 12), } satisfies AnchorPosition), createMenuPositionProfile: (windowWidth: number) => @@ -2819,7 +2821,7 @@ const styles = (theme: ThemeColors) => smallEditIcon: { alignItems: 'center', backgroundColor: theme.buttonHoveredBG, - borderColor: theme.textReversed, + borderColor: theme.appBG, borderRadius: 14, borderWidth: 3, color: theme.textReversed, @@ -3997,8 +3999,7 @@ const styles = (theme: ThemeColors) => type ThemeStyles = ReturnType; -const stylesGenerator = styles; const defaultStyles = styles(defaultTheme); -export default defaultStyles; -export {stylesGenerator, type Styles, type ThemeStyles, type StatusBarStyle, type ColorScheme}; +export default styles; +export {defaultStyles, type Styles, type ThemeStyles, type StatusBarStyle, type ColorScheme}; diff --git a/src/styles/themes/ThemeContext.ts b/src/styles/themes/ThemeContext.ts index 3c969c7393c5..ec35675953fe 100644 --- a/src/styles/themes/ThemeContext.ts +++ b/src/styles/themes/ThemeContext.ts @@ -1,7 +1,7 @@ import React from 'react'; -import darkTheme from './default'; +import {defaultTheme} from './themes'; import {type ThemeColors} from './types'; -const ThemeContext = React.createContext(darkTheme); +const ThemeContext = React.createContext(defaultTheme); export default ThemeContext; diff --git a/src/styles/themes/ThemeProvider.tsx b/src/styles/themes/ThemeProvider.tsx index 20f97025c36a..0d302b5ae056 100644 --- a/src/styles/themes/ThemeProvider.tsx +++ b/src/styles/themes/ThemeProvider.tsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; import ThemeContext from './ThemeContext'; -import Themes from './Themes'; +import themes from './themes'; import {ThemePreferenceWithoutSystem} from './types'; import useThemePreferenceWithStaticOverride from './useThemePreferenceWithStaticOverride'; @@ -18,7 +18,7 @@ type ThemeProviderProps = React.PropsWithChildren & { function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderProps) { const themePreference = useThemePreferenceWithStaticOverride(staticThemePreference); - const theme = useMemo(() => Themes[themePreference], [themePreference]); + const theme = useMemo(() => themes[themePreference], [themePreference]); return {children}; } diff --git a/src/styles/themes/default.ts b/src/styles/themes/default.ts index 59de9b7269a8..891eb1e9bf8e 100644 --- a/src/styles/themes/default.ts +++ b/src/styles/themes/default.ts @@ -5,26 +5,26 @@ import {type ThemeColors} from './types'; const darkTheme = { // Figma keys - appBG: colors.darkAppBackground, + appBG: colors.productDark100, splashBG: colors.green400, - highlightBG: colors.darkHighlightBackground, - border: colors.darkBorders, - borderLighter: colors.darkDefaultButton, + highlightBG: colors.productDark200, + border: colors.productDark400, + borderLighter: colors.productDark400, borderFocus: colors.green400, - icon: colors.darkIcons, + icon: colors.productDark700, iconMenu: colors.green400, - iconHovered: colors.darkPrimaryText, + iconHovered: colors.productDark900, iconSuccessFill: colors.green400, - iconReversed: colors.darkAppBackground, + iconReversed: colors.productDark100, iconColorfulBackground: `${colors.ivory}cc`, - textSupporting: colors.darkSupportingText, - text: colors.darkPrimaryText, + textSupporting: colors.productDark800, + text: colors.productDark900, textColorfulBackground: colors.ivory, link: colors.blue300, linkHover: colors.blue100, - buttonDefaultBG: colors.darkDefaultButton, - buttonHoveredBG: colors.darkDefaultButtonHover, - buttonPressedBG: colors.darkDefaultButtonPressed, + buttonDefaultBG: colors.productDark400, + buttonHoveredBG: colors.productDark500, + buttonPressedBG: colors.productDark600, danger: colors.red, dangerHover: colors.redHover, dangerPressed: colors.redHover, @@ -37,36 +37,36 @@ const darkTheme = { dangerSection: colors.tangerine800, // Additional keys - overlay: colors.darkBorders, - inverse: colors.darkPrimaryText, + overlay: colors.productDark400, + inverse: colors.productDark900, shadow: colors.black, - componentBG: colors.darkAppBackground, - hoverComponentBG: colors.darkHighlightBackground, - activeComponentBG: colors.darkBorders, + componentBG: colors.productDark100, + hoverComponentBG: colors.productDark200, + activeComponentBG: colors.productDark400, signInSidebar: colors.green800, - sidebar: colors.darkHighlightBackground, - sidebarHover: colors.darkAppBackground, - heading: colors.darkPrimaryText, - textLight: colors.darkPrimaryText, - textDark: colors.darkAppBackground, - textReversed: colors.lightPrimaryText, - textBackground: colors.darkHighlightBackground, - textMutedReversed: colors.darkIcons, + sidebar: colors.productDark200, + sidebarHover: colors.productDark300, + heading: colors.productDark900, + textLight: colors.productDark900, + textDark: colors.productDark100, + textReversed: colors.productLight900, + textBackground: colors.productDark200, + textMutedReversed: colors.productDark700, textError: colors.red, - offline: colors.darkIcons, - modalBackground: colors.darkAppBackground, - cardBG: colors.darkHighlightBackground, - cardBorder: colors.darkHighlightBackground, - spinner: colors.darkSupportingText, + offline: colors.productDark700, + modalBackground: colors.productDark100, + cardBG: colors.productDark200, + cardBorder: colors.productDark200, + spinner: colors.productDark800, unreadIndicator: colors.green400, - placeholderText: colors.darkIcons, + placeholderText: colors.productDark700, heroCard: colors.blue400, - uploadPreviewActivityIndicator: colors.darkHighlightBackground, + uploadPreviewActivityIndicator: colors.productDark200, dropUIBG: 'rgba(6,27,9,0.92)', receiptDropUIBG: 'rgba(3, 212, 124, 0.84)', checkBox: colors.green400, - pickerOptionsTextColor: colors.darkPrimaryText, - imageCropBackgroundColor: colors.darkIcons, + pickerOptionsTextColor: colors.productDark900, + imageCropBackgroundColor: colors.productDark700, fallbackIconColor: colors.green700, reactionActiveBackground: colors.green600, reactionActiveText: colors.green100, @@ -76,10 +76,10 @@ const darkTheme = { mentionBG: colors.blue600, ourMentionText: colors.green100, ourMentionBG: colors.green600, - tooltipSupportingText: colors.lightSupportingText, - tooltipPrimaryText: colors.lightPrimaryText, - skeletonLHNIn: colors.darkBorders, - skeletonLHNOut: colors.darkDefaultButton, + tooltipSupportingText: colors.productLight800, + tooltipPrimaryText: colors.productLight900, + skeletonLHNIn: colors.productDark400, + skeletonLHNOut: colors.productDark400, QRLogo: colors.green400, starDefaultBG: 'rgb(254, 228, 94)', loungeAccessOverlay: colors.blue800, @@ -92,11 +92,11 @@ const darkTheme = { // The screen name (see SCREENS.ts) is the name of the screen as far as react-navigation is concerned, and the linkingConfig maps screen names to URLs PAGE_THEMES: { [SCREENS.HOME]: { - backgroundColor: colors.darkHighlightBackground, + backgroundColor: colors.productDark200, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.REPORT]: { - backgroundColor: colors.darkAppBackground, + backgroundColor: colors.productDark100, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.SAVE_THE_WORLD.ROOT]: { @@ -112,7 +112,7 @@ const darkTheme = { statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.SETTINGS.WALLET]: { - backgroundColor: colors.darkAppBackground, + backgroundColor: colors.productDark100, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.SETTINGS.SECURITY]: { @@ -124,7 +124,7 @@ const darkTheme = { statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.SETTINGS.ROOT]: { - backgroundColor: colors.darkHighlightBackground, + backgroundColor: colors.productDark200, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, }, diff --git a/src/styles/themes/light.ts b/src/styles/themes/light.ts index 1541c0e723d7..b3f71dec8ac2 100644 --- a/src/styles/themes/light.ts +++ b/src/styles/themes/light.ts @@ -5,26 +5,26 @@ import {type ThemeColors} from './types'; const lightTheme = { // Figma keys - appBG: colors.lightAppBackground, + appBG: colors.productLight100, splashBG: colors.green400, - highlightBG: colors.lightHighlightBackground, - border: colors.lightBorders, - borderLighter: colors.lightDefaultButtonPressed, + highlightBG: colors.productLight200, + border: colors.productLight400, + borderLighter: colors.productLight600, borderFocus: colors.green400, - icon: colors.lightIcons, + icon: colors.productLight700, iconMenu: colors.green400, - iconHovered: colors.lightPrimaryText, + iconHovered: colors.productLight900, iconSuccessFill: colors.green400, - iconReversed: colors.lightAppBackground, + iconReversed: colors.productLight100, iconColorfulBackground: `${colors.ivory}cc`, - textSupporting: colors.lightSupportingText, - text: colors.lightPrimaryText, + textSupporting: colors.productLight800, + text: colors.productLight900, textColorfulBackground: colors.ivory, link: colors.blue600, linkHover: colors.blue500, - buttonDefaultBG: colors.lightDefaultButton, - buttonHoveredBG: colors.lightDefaultButtonHover, - buttonPressedBG: colors.lightDefaultButtonPressed, + buttonDefaultBG: colors.productLight400, + buttonHoveredBG: colors.productLight500, + buttonPressedBG: colors.productLight600, danger: colors.red, dangerHover: colors.redHover, dangerPressed: colors.redHover, @@ -37,36 +37,36 @@ const lightTheme = { dangerSection: colors.tangerine800, // Additional keys - overlay: colors.lightBorders, - inverse: colors.lightPrimaryText, + overlay: colors.productLight400, + inverse: colors.productLight900, shadow: colors.black, - componentBG: colors.lightAppBackground, - hoverComponentBG: colors.lightHighlightBackground, - activeComponentBG: colors.lightBorders, + componentBG: colors.productLight100, + hoverComponentBG: colors.productLight200, + activeComponentBG: colors.productLight400, signInSidebar: colors.green800, - sidebar: colors.lightHighlightBackground, - sidebarHover: colors.lightBorders, - heading: colors.lightPrimaryText, + sidebar: colors.productLight200, + sidebarHover: colors.productLight300, + heading: colors.productLight900, textLight: colors.white, - textDark: colors.lightPrimaryText, - textReversed: colors.darkPrimaryText, - textBackground: colors.lightHighlightBackground, - textMutedReversed: colors.lightIcons, + textDark: colors.productLight900, + textReversed: colors.productDark900, + textBackground: colors.productLight200, + textMutedReversed: colors.productLight700, textError: colors.red, - offline: colors.lightIcons, - modalBackground: colors.lightAppBackground, - cardBG: colors.lightHighlightBackground, - cardBorder: colors.lightHighlightBackground, - spinner: colors.lightSupportingText, + offline: colors.productLight700, + modalBackground: colors.productLight100, + cardBG: colors.productLight200, + cardBorder: colors.productLight200, + spinner: colors.productLight800, unreadIndicator: colors.green400, - placeholderText: colors.lightIcons, + placeholderText: colors.productLight700, heroCard: colors.blue400, - uploadPreviewActivityIndicator: colors.lightHighlightBackground, + uploadPreviewActivityIndicator: colors.productLight200, dropUIBG: 'rgba(252, 251, 249, 0.92)', receiptDropUIBG: 'rgba(3, 212, 124, 0.84)', checkBox: colors.green400, - pickerOptionsTextColor: colors.lightPrimaryText, - imageCropBackgroundColor: colors.lightIcons, + pickerOptionsTextColor: colors.productLight900, + imageCropBackgroundColor: colors.productLight700, fallbackIconColor: colors.green700, reactionActiveBackground: colors.green100, reactionActiveText: colors.green600, @@ -76,10 +76,10 @@ const lightTheme = { mentionBG: colors.blue100, ourMentionText: colors.green600, ourMentionBG: colors.green100, - tooltipSupportingText: colors.darkSupportingText, - tooltipPrimaryText: colors.darkPrimaryText, - skeletonLHNIn: colors.lightBorders, - skeletonLHNOut: colors.lightDefaultButtonPressed, + tooltipSupportingText: colors.productDark800, + tooltipPrimaryText: colors.productDark900, + skeletonLHNIn: colors.productLight400, + skeletonLHNOut: colors.productLight600, QRLogo: colors.green400, starDefaultBG: 'rgb(254, 228, 94)', loungeAccessOverlay: colors.blue800, @@ -92,11 +92,11 @@ const lightTheme = { // The screen name (see SCREENS.ts) is the name of the screen as far as react-navigation is concerned, and the linkingConfig maps screen names to URLs PAGE_THEMES: { [SCREENS.HOME]: { - backgroundColor: colors.lightHighlightBackground, + backgroundColor: colors.productLight200, statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT, }, [SCREENS.REPORT]: { - backgroundColor: colors.lightAppBackground, + backgroundColor: colors.productLight100, statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT, }, [SCREENS.SAVE_THE_WORLD.ROOT]: { @@ -112,7 +112,7 @@ const lightTheme = { statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.SETTINGS.WALLET]: { - backgroundColor: colors.darkAppBackground, + backgroundColor: colors.productDark100, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.SETTINGS.SECURITY]: { @@ -124,7 +124,7 @@ const lightTheme = { statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, [SCREENS.SETTINGS.ROOT]: { - backgroundColor: colors.lightHighlightBackground, + backgroundColor: colors.productLight200, statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT, }, }, diff --git a/src/styles/themes/Themes.ts b/src/styles/themes/themes.ts similarity index 73% rename from src/styles/themes/Themes.ts rename to src/styles/themes/themes.ts index 5de65c7316b4..c0a305df294f 100644 --- a/src/styles/themes/Themes.ts +++ b/src/styles/themes/themes.ts @@ -3,9 +3,12 @@ import darkTheme from './default'; import lightTheme from './light'; import {type ThemeColors, ThemePreferenceWithoutSystem} from './types'; -const Themes = { +const themes = { [CONST.THEME.LIGHT]: lightTheme, [CONST.THEME.DARK]: darkTheme, } satisfies Record; -export default Themes; +const defaultTheme = themes[CONST.THEME.DEFAULT]; + +export default themes; +export {defaultTheme}; diff --git a/src/styles/useStyleUtils.ts b/src/styles/useStyleUtils.ts new file mode 100644 index 000000000000..aadb3f884220 --- /dev/null +++ b/src/styles/useStyleUtils.ts @@ -0,0 +1,14 @@ +import {useContext} from 'react'; +import ThemeStylesContext from './ThemeStylesContext'; + +function useStyleUtils() { + const themeStylesContext = useContext(ThemeStylesContext); + + if (!themeStylesContext) { + throw new Error('ThemeStylesContext was null! Are you sure that you wrapped the component under a ?'); + } + + return themeStylesContext.StyleUtils; +} + +export default useStyleUtils; diff --git a/src/styles/useThemeStyles.ts b/src/styles/useThemeStyles.ts index 06a4f7f5d626..164806a908e4 100644 --- a/src/styles/useThemeStyles.ts +++ b/src/styles/useThemeStyles.ts @@ -2,13 +2,13 @@ import {useContext} from 'react'; import ThemeStylesContext from './ThemeStylesContext'; function useThemeStyles() { - const themeStyles = useContext(ThemeStylesContext); + const themeStylesContext = useContext(ThemeStylesContext); - if (!themeStyles) { + if (!themeStylesContext) { throw new Error('ThemeStylesContext was null! Are you sure that you wrapped the component under a ?'); } - return themeStyles; + return themeStylesContext.styles; } export default useThemeStyles; diff --git a/src/styles/utils/ModalStyleUtils.ts b/src/styles/utils/ModalStyleUtils.ts new file mode 100644 index 000000000000..cf312a4e76f7 --- /dev/null +++ b/src/styles/utils/ModalStyleUtils.ts @@ -0,0 +1,273 @@ +import {ViewStyle} from 'react-native'; +import {ModalProps} from 'react-native-modal'; +import {ValueOf} from 'type-fest'; +import {type ThemeStyles} from '@styles/styles'; +import {type ThemeColors} from '@styles/themes/types'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; + +function getCenteredModalStyles(styles: ThemeStyles, windowWidth: number, isSmallScreenWidth: boolean, isFullScreenWhenSmall = false): ViewStyle { + const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, isFullScreenWhenSmall); + + return { + borderWidth: modalStyles.borderWidth, + width: isSmallScreenWidth ? '100%' : windowWidth - modalStyles.marginHorizontal * 2, + }; +} + +type ModalType = ValueOf; + +type WindowDimensions = { + windowWidth: number; + windowHeight: number; + isSmallScreenWidth: boolean; +}; + +type GetModalStyles = { + modalStyle: ViewStyle; + modalContainerStyle: ViewStyle; + swipeDirection: ModalProps['swipeDirection']; + animationIn: ModalProps['animationIn']; + animationOut: ModalProps['animationOut']; + hideBackdrop: boolean; + shouldAddTopSafeAreaMargin: boolean; + shouldAddBottomSafeAreaMargin: boolean; + shouldAddBottomSafeAreaPadding: boolean; + shouldAddTopSafeAreaPadding: boolean; +}; + +const createModalStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ + getModalStyles: ( + type: ModalType | undefined, + windowDimensions: WindowDimensions, + popoverAnchorPosition: ViewStyle = {}, + innerContainerStyle: ViewStyle = {}, + outerStyle: ViewStyle = {}, + ): GetModalStyles => { + const {isSmallScreenWidth, windowWidth} = windowDimensions; + + let modalStyle: GetModalStyles['modalStyle'] = { + margin: 0, + ...outerStyle, + }; + + let modalContainerStyle: GetModalStyles['modalContainerStyle']; + let swipeDirection: GetModalStyles['swipeDirection']; + let animationIn: GetModalStyles['animationIn']; + let animationOut: GetModalStyles['animationOut']; + let hideBackdrop = false; + let shouldAddBottomSafeAreaMargin = false; + let shouldAddTopSafeAreaMargin = false; + let shouldAddBottomSafeAreaPadding = false; + let shouldAddTopSafeAreaPadding = false; + + switch (type) { + case CONST.MODAL.MODAL_TYPE.CONFIRM: + // A confirm modal is one that has a visible backdrop + // and can be dismissed by clicking outside of the modal. + modalStyle = { + ...modalStyle, + ...{ + alignItems: 'center', + }, + }; + modalContainerStyle = { + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', + borderRadius: 12, + overflow: 'hidden', + width: variables.sideBarWidth, + }; + + // setting this to undefined we effectively disable the + // ability to swipe our modal + swipeDirection = undefined; + animationIn = 'fadeIn'; + animationOut = 'fadeOut'; + break; + case CONST.MODAL.MODAL_TYPE.CENTERED: + // A centered modal is one that has a visible backdrop + // and can be dismissed by clicking outside of the modal. + // This modal should take up the entire visible area when + // viewed on a smaller device (e.g. mobile or mobile web). + modalStyle = { + ...modalStyle, + ...{ + alignItems: 'center', + }, + }; + modalContainerStyle = { + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', + flex: 1, + marginTop: isSmallScreenWidth ? 0 : 20, + marginBottom: isSmallScreenWidth ? 0 : 20, + borderRadius: isSmallScreenWidth ? 0 : 12, + overflow: 'hidden', + ...getCenteredModalStyles(styles, windowWidth, isSmallScreenWidth), + }; + + // Allow this modal to be dismissed with a swipe down or swipe right + swipeDirection = ['down', 'right']; + animationIn = isSmallScreenWidth ? 'slideInRight' : 'fadeIn'; + animationOut = isSmallScreenWidth ? 'slideOutRight' : 'fadeOut'; + shouldAddTopSafeAreaMargin = !isSmallScreenWidth; + shouldAddBottomSafeAreaMargin = !isSmallScreenWidth; + shouldAddTopSafeAreaPadding = isSmallScreenWidth; + shouldAddBottomSafeAreaPadding = false; + break; + case CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE: + // A centered modal that cannot be dismissed with a swipe. + modalStyle = { + ...modalStyle, + ...{ + alignItems: 'center', + }, + }; + modalContainerStyle = { + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', + flex: 1, + marginTop: isSmallScreenWidth ? 0 : 20, + marginBottom: isSmallScreenWidth ? 0 : 20, + borderRadius: isSmallScreenWidth ? 0 : 12, + overflow: 'hidden', + ...getCenteredModalStyles(styles, windowWidth, isSmallScreenWidth, true), + }; + swipeDirection = undefined; + animationIn = isSmallScreenWidth ? 'slideInRight' : 'fadeIn'; + animationOut = isSmallScreenWidth ? 'slideOutRight' : 'fadeOut'; + shouldAddTopSafeAreaMargin = !isSmallScreenWidth; + shouldAddBottomSafeAreaMargin = !isSmallScreenWidth; + shouldAddTopSafeAreaPadding = isSmallScreenWidth; + shouldAddBottomSafeAreaPadding = false; + break; + case CONST.MODAL.MODAL_TYPE.CENTERED_SMALL: + // A centered modal that takes up the minimum possible screen space on all devices + modalStyle = { + ...modalStyle, + ...{ + alignItems: 'center', + }, + }; + modalContainerStyle = { + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', + borderRadius: 12, + borderWidth: 0, + }; + + // Allow this modal to be dismissed with a swipe down or swipe right + swipeDirection = ['down', 'right']; + animationIn = 'fadeIn'; + animationOut = 'fadeOut'; + shouldAddTopSafeAreaMargin = false; + shouldAddBottomSafeAreaMargin = false; + shouldAddTopSafeAreaPadding = false; + shouldAddBottomSafeAreaPadding = false; + break; + case CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED: + modalStyle = { + ...modalStyle, + ...{ + alignItems: 'center', + justifyContent: 'flex-end', + }, + }; + modalContainerStyle = { + width: '100%', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingTop: 12, + justifyContent: 'center', + overflow: 'hidden', + }; + + shouldAddBottomSafeAreaPadding = true; + swipeDirection = undefined; + animationIn = 'slideInUp'; + animationOut = 'slideOutDown'; + break; + case CONST.MODAL.MODAL_TYPE.POPOVER: + modalStyle = { + ...modalStyle, + ...popoverAnchorPosition, + ...{ + position: 'absolute', + alignItems: 'center', + justifyContent: 'flex-end', + }, + }; + modalContainerStyle = { + borderRadius: 12, + borderWidth: 1, + borderColor: theme.border, + justifyContent: 'center', + overflow: 'hidden', + boxShadow: variables.popoverMenuShadow, + }; + + hideBackdrop = true; + swipeDirection = undefined; + animationIn = 'fadeIn'; + animationOut = 'fadeOut'; + break; + case CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED: + modalStyle = { + ...modalStyle, + ...{ + marginLeft: isSmallScreenWidth ? 0 : windowWidth - variables.sideBarWidth, + width: isSmallScreenWidth ? '100%' : variables.sideBarWidth, + flexDirection: 'row', + justifyContent: 'flex-end', + }, + }; + modalContainerStyle = { + width: isSmallScreenWidth ? '100%' : variables.sideBarWidth, + height: '100%', + overflow: 'hidden', + }; + + animationIn = { + from: { + translateX: isSmallScreenWidth ? windowWidth : variables.sideBarWidth, + }, + to: { + translateX: 0, + }, + }; + animationOut = { + from: { + translateX: 0, + }, + to: { + translateX: isSmallScreenWidth ? windowWidth : variables.sideBarWidth, + }, + }; + hideBackdrop = true; + swipeDirection = undefined; + shouldAddBottomSafeAreaPadding = true; + shouldAddTopSafeAreaPadding = true; + break; + default: + modalStyle = {}; + modalContainerStyle = {}; + swipeDirection = 'down'; + animationIn = 'slideInUp'; + animationOut = 'slideOutDown'; + } + + modalContainerStyle = {...modalContainerStyle, ...innerContainerStyle}; + + return { + modalStyle, + modalContainerStyle, + swipeDirection, + animationIn, + animationOut, + hideBackdrop, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaMargin, + shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaPadding, + }; + }, +}); + +export default createModalStyleUtils; diff --git a/src/styles/getReportActionContextMenuStyles.ts b/src/styles/utils/ReportActionContextMenuStyleUtils.ts similarity index 55% rename from src/styles/getReportActionContextMenuStyles.ts rename to src/styles/utils/ReportActionContextMenuStyleUtils.ts index 86dd14cb8446..9dd3c2a0d203 100644 --- a/src/styles/getReportActionContextMenuStyles.ts +++ b/src/styles/utils/ReportActionContextMenuStyleUtils.ts @@ -1,7 +1,7 @@ import {ViewStyle} from 'react-native'; -import {type ThemeStyles} from './styles'; -import {type ThemeColors} from './themes/types'; -import variables from './variables'; +import {type ThemeStyles} from '@styles/styles'; +import {type ThemeColors} from '@styles/themes/types'; +import variables from '@styles/variables'; const getDefaultWrapperStyle = (theme: ThemeColors): ViewStyle => ({ backgroundColor: theme.componentBG, @@ -27,18 +27,20 @@ const getMiniWrapperStyle = (theme: ThemeColors, styles: ThemeStyles): ViewStyle * @param isSmallScreenWidth * @param theme */ -function getReportActionContextMenuStyles(styles: ThemeStyles, isMini: boolean, isSmallScreenWidth: boolean, theme: ThemeColors): ViewStyle[] { - if (isMini) { - return getMiniWrapperStyle(theme, styles); - } +const createReportActionContextMenuStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ + getReportActionContextMenuStyles: (isMini: boolean, isSmallScreenWidth: boolean): ViewStyle[] => { + if (isMini) { + return getMiniWrapperStyle(theme, styles); + } - return [ - styles.flexColumn, - getDefaultWrapperStyle(theme), + return [ + styles.flexColumn, + getDefaultWrapperStyle(theme), - // Small screens use a bottom-docked modal that already has vertical padding. - isSmallScreenWidth ? {} : styles.pv3, - ]; -} + // Small screens use a bottom-docked modal that already has vertical padding. + isSmallScreenWidth ? {} : styles.pv3, + ]; + }, +}); -export default getReportActionContextMenuStyles; +export default createReportActionContextMenuStyleUtils; diff --git a/src/styles/utils/TooltipStyleUtils.ts b/src/styles/utils/TooltipStyleUtils.ts new file mode 100644 index 000000000000..aefd6ae54d2f --- /dev/null +++ b/src/styles/utils/TooltipStyleUtils.ts @@ -0,0 +1,303 @@ +import {TextStyle, View, ViewStyle} from 'react-native'; +import fontFamily from '@styles/fontFamily'; +import {type ThemeStyles} from '@styles/styles'; +import {type ThemeColors} from '@styles/themes/types'; +import positioning from '@styles/utilities/positioning'; +import spacing from '@styles/utilities/spacing'; +import variables from '@styles/variables'; +import roundToNearestMultipleOfFour from './roundToNearestMultipleOfFour'; + +/** This defines the proximity with the edge of the window in which tooltips should not be displayed. + * If a tooltip is too close to the edge of the screen, we'll shift it towards the center. */ +const GUTTER_WIDTH = variables.gutterWidth; + +/** The height of a tooltip pointer */ +const POINTER_HEIGHT = 4; + +/** The width of a tooltip pointer */ +const POINTER_WIDTH = 12; + +/** + * Compute the amount the tooltip needs to be horizontally shifted in order to keep it from displaying in the gutters. + * + * @param windowWidth - The width of the window. + * @param xOffset - The distance between the left edge of the window + * and the left edge of the wrapped component. + * @param componentWidth - The width of the wrapped component. + * @param tooltipWidth - The width of the tooltip itself. + * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. + * A positive value shifts it to the right, + * and a negative value shifts it to the left. + */ +function computeHorizontalShift(windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number): number { + // First find the left and right edges of the tooltip (by default, it is centered on the component). + const componentCenter = xOffset + componentWidth / 2 + manualShiftHorizontal; + const tooltipLeftEdge = componentCenter - tooltipWidth / 2; + const tooltipRightEdge = componentCenter + tooltipWidth / 2; + + if (tooltipLeftEdge < GUTTER_WIDTH) { + // Tooltip is in left gutter, shift right by a multiple of four. + return roundToNearestMultipleOfFour(GUTTER_WIDTH - tooltipLeftEdge); + } + + if (tooltipRightEdge > windowWidth - GUTTER_WIDTH) { + // Tooltip is in right gutter, shift left by a multiple of four. + return roundToNearestMultipleOfFour(windowWidth - GUTTER_WIDTH - tooltipRightEdge); + } + + // Tooltip is not in the gutter, so no need to shift it horizontally + return 0; +} + +/** + * Determines if there is an overlapping element at the top of a given coordinate. + * (targetCenterX, y) + * | + * v + * _ _ _ _ _ + * | | + * | | + * | | + * | | + * |_ _ _ _ _| + * + * @param tooltip - The reference to the tooltip's root element + * @param xOffset - The distance between the left edge of the window + * and the left edge of the wrapped component. + * @param yOffset - The distance between the top edge of the window + * and the top edge of the wrapped component. + * @param tooltipTargetWidth - The width of the tooltip's target + * @param tooltipTargetHeight - The height of the tooltip's target + */ +function isOverlappingAtTop(tooltip: View | HTMLDivElement, xOffset: number, yOffset: number, tooltipTargetWidth: number, tooltipTargetHeight: number) { + if (typeof document.elementFromPoint !== 'function') { + return false; + } + + // Use the x center position of the target to prevent wrong element returned by elementFromPoint + // in case the target has a border radius or is a multiline text. + const targetCenterX = xOffset + tooltipTargetWidth / 2; + const elementAtTargetCenterX = document.elementFromPoint(targetCenterX, yOffset); + + // Ensure it's not the already rendered element of this very tooltip, so the tooltip doesn't try to "avoid" itself + if (!elementAtTargetCenterX || ('contains' in tooltip && tooltip.contains(elementAtTargetCenterX))) { + return false; + } + + const rectAtTargetCenterX = elementAtTargetCenterX.getBoundingClientRect(); + + // Ensure it's not overlapping with another element by checking if the yOffset is greater than the top of the element + // and less than the bottom of the element. Also ensure the tooltip target is not completely inside the elementAtTargetCenterX by vertical direction + const isOverlappingAtTargetCenterX = yOffset > rectAtTargetCenterX.top && yOffset < rectAtTargetCenterX.bottom && yOffset + tooltipTargetHeight > rectAtTargetCenterX.bottom; + + return isOverlappingAtTargetCenterX; +} + +type TooltipStyles = { + animationStyle: ViewStyle; + rootWrapperStyle: ViewStyle; + textStyle: TextStyle; + pointerWrapperStyle: ViewStyle; + pointerStyle: ViewStyle; +}; + +type TooltipParams = { + tooltip: View | HTMLDivElement; + currentSize: number; + windowWidth: number; + xOffset: number; + yOffset: number; + tooltipTargetWidth: number; + tooltipTargetHeight: number; + maxWidth: number; + tooltipContentWidth: number; + tooltipWrapperHeight: number; + theme: ThemeColors; + styles: ThemeStyles; + manualShiftHorizontal?: number; + manualShiftVertical?: number; +}; + +/** + * Generate styles for the tooltip component. + * + * @param tooltip - The reference to the tooltip's root element + * @param currentSize - The current size of the tooltip used in the scaling animation. + * @param windowWidth - The width of the window. + * @param xOffset - The distance between the left edge of the window + * and the left edge of the wrapped component. + * @param yOffset - The distance between the top edge of the window + * and the top edge of the wrapped component. + * @param tooltipTargetWidth - The width of the tooltip's target + * @param tooltipTargetHeight - The height of the tooltip's target + * @param maxWidth - The tooltip's max width. + * @param tooltipContentWidth - The tooltip's inner content measured width. + * @param tooltipWrapperHeight - The tooltip's wrapper measured height. + * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. + * A positive value shifts it to the right, + * and a negative value shifts it to the left. + * @param [manualShiftVertical] - Any additional amount to manually shift the tooltip up or down. + * A positive value shifts it down, and a negative value shifts it up. + */ +const createTooltipStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ + getTooltipStyles: ({ + tooltip, + currentSize, + windowWidth, + xOffset, + yOffset, + tooltipTargetWidth, + tooltipTargetHeight, + maxWidth, + tooltipContentWidth, + tooltipWrapperHeight, + manualShiftHorizontal = 0, + manualShiftVertical = 0, + }: TooltipParams): TooltipStyles => { + const tooltipVerticalPadding = spacing.pv1; + + // We calculate tooltip width based on the tooltip's content width + // so the tooltip wrapper is just big enough to fit content and prevent white space. + // NOTE: Add 1 to the tooltipWidth to prevent truncated text in Safari + const tooltipWidth = tooltipContentWidth && tooltipContentWidth + spacing.ph2.paddingHorizontal * 2 + 1; + const tooltipHeight = tooltipWrapperHeight; + + const isTooltipSizeReady = tooltipWidth !== undefined && tooltipHeight !== undefined; + + // Set the scale to 1 to be able to measure the tooltip size correctly when it's not ready yet. + let scale = 1; + let shouldShowBelow = false; + let horizontalShift = 0; + let horizontalShiftPointer = 0; + let rootWrapperTop = 0; + let rootWrapperLeft = 0; + let pointerWrapperTop = 0; + let pointerWrapperLeft = 0; + let pointerAdditionalStyle = {}; + + if (isTooltipSizeReady) { + // Determine if the tooltip should display below the wrapped component. + // If either a tooltip will try to render within GUTTER_WIDTH logical pixels of the top of the screen, + // Or the wrapped component is overlapping at top-center with another element + // we'll display it beneath its wrapped component rather than above it as usual. + shouldShowBelow = yOffset - tooltipHeight < GUTTER_WIDTH || isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight); + + // When the tooltip size is ready, we can start animating the scale. + scale = currentSize; + + // Determine if we need to shift the tooltip horizontally to prevent it + // from displaying too near to the edge of the screen. + horizontalShift = computeHorizontalShift(windowWidth, xOffset, tooltipTargetWidth, tooltipWidth, manualShiftHorizontal); + + // Determine if we need to shift the pointer horizontally to prevent it from being too near to the edge of the tooltip + // We shift it to the right a bit if the tooltip is positioned on the extreme left + // and shift it to left a bit if the tooltip is positioned on the extreme right. + horizontalShiftPointer = + horizontalShift > 0 + ? Math.max(-horizontalShift, -(tooltipWidth / 2) + POINTER_WIDTH / 2 + variables.componentBorderRadiusSmall) + : Math.min(-horizontalShift, tooltipWidth / 2 - POINTER_WIDTH / 2 - variables.componentBorderRadiusSmall); + + // Because it uses fixed positioning, the top-left corner of the tooltip is aligned + // with the top-left corner of the window by default. + // we will use yOffset to position the tooltip relative to the Wrapped Component + // So we need to shift the tooltip vertically and horizontally to position it correctly. + // + // First, we'll position it vertically. + // To shift the tooltip down, we'll give `top` a positive value. + // To shift the tooltip up, we'll give `top` a negative value. + rootWrapperTop = shouldShowBelow + ? // We need to shift the tooltip down below the component. So shift the tooltip down (+) by... + yOffset + tooltipTargetHeight + POINTER_HEIGHT + manualShiftVertical + : // We need to shift the tooltip up above the component. So shift the tooltip up (-) by... + yOffset - (tooltipHeight + POINTER_HEIGHT) + manualShiftVertical; + + // Next, we'll position it horizontally. + // we will use xOffset to position the tooltip relative to the Wrapped Component + // To shift the tooltip right, we'll give `left` a positive value. + // To shift the tooltip left, we'll give `left` a negative value. + // + // So we'll: + // 1) Shift the tooltip right (+) to the center of the component, + // so the left edge lines up with the component center. + // 2) Shift it left (-) to by half the tooltip's width, + // so the tooltip's center lines up with the center of the wrapped component. + // 3) Add the horizontal shift (left or right) computed above to keep it out of the gutters. + // 4) Lastly, add the manual horizontal shift passed in as a parameter. + rootWrapperLeft = xOffset + (tooltipTargetWidth / 2 - tooltipWidth / 2) + horizontalShift + manualShiftHorizontal; + + // By default, the pointer's top-left will align with the top-left of the tooltip wrapper. + // + // To align it vertically, we'll: + // If the pointer should be below the tooltip wrapper, shift the pointer down (+) by the tooltip height, + // so that the top of the pointer lines up with the bottom of the tooltip + // + // OR if the pointer should be above the tooltip wrapper, then the pointer up (-) by the pointer's height + // so that the bottom of the pointer lines up with the top of the tooltip + pointerWrapperTop = shouldShowBelow ? -POINTER_HEIGHT : tooltipHeight; + + // To align it horizontally, we'll: + // 1) Shift the pointer to the right (+) by the half the tooltipWidth's width, + // so the left edge of the pointer lines up with the tooltipWidth's center. + // 2) To the left (-) by half the pointer's width, + // so the pointer's center lines up with the tooltipWidth's center. + // 3) Remove the wrapper's horizontalShift to maintain the pointer + // at the center of the hovered component. + pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2); + + pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {}; + } + + return { + animationStyle: { + // remember Transform causes a new Local cordinate system + // https://drafts.csswg.org/css-transforms-1/#transform-rendering + // so Position fixed children will be relative to this new Local cordinate system + transform: [{scale}], + }, + rootWrapperStyle: { + ...positioning.pFixed, + backgroundColor: theme.heading, + borderRadius: variables.componentBorderRadiusSmall, + ...tooltipVerticalPadding, + ...spacing.ph2, + zIndex: variables.tooltipzIndex, + width: tooltipWidth, + maxWidth, + top: rootWrapperTop, + left: rootWrapperLeft, + + // We are adding this to prevent the tooltip text from being selected and copied on CTRL + A. + ...styles.userSelectNone, + ...styles.pointerEventsNone, + }, + textStyle: { + color: theme.textReversed, + fontFamily: fontFamily.EXP_NEUE, + fontSize: variables.fontSizeSmall, + overflow: 'hidden', + lineHeight: variables.lineHeightSmall, + textAlign: 'center', + }, + pointerWrapperStyle: { + ...positioning.pFixed, + top: pointerWrapperTop, + left: pointerWrapperLeft, + }, + pointerStyle: { + width: 0, + height: 0, + backgroundColor: theme.transparent, + borderStyle: 'solid', + borderLeftWidth: POINTER_WIDTH / 2, + borderRightWidth: POINTER_WIDTH / 2, + borderTopWidth: POINTER_HEIGHT, + borderLeftColor: theme.transparent, + borderRightColor: theme.transparent, + borderTopColor: theme.heading, + ...pointerAdditionalStyle, + }, + }; + }, +}); + +export default createTooltipStyleUtils; diff --git a/src/styles/StyleUtils.ts b/src/styles/utils/index.ts similarity index 74% rename from src/styles/StyleUtils.ts rename to src/styles/utils/index.ts index 64ef54a8ab84..8d52c8de200a 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/utils/index.ts @@ -4,25 +4,26 @@ import {EdgeInsets} from 'react-native-safe-area-context'; import {ValueOf} from 'type-fest'; import * as Browser from '@libs/Browser'; import * as UserUtils from '@libs/UserUtils'; +import colors from '@styles/colors'; +import containerComposeStyles from '@styles/containerComposeStyles'; +import fontFamily from '@styles/fontFamily'; +import getContextMenuItemStyles from '@styles/getContextMenuItemStyles'; +import {compactContentContainerStyles} from '@styles/optionRowStyles'; +import {defaultStyles, type ThemeStyles} from '@styles/styles'; +import {defaultTheme} from '@styles/themes/themes'; +import {ThemeColors} from '@styles/themes/types'; +import cursor from '@styles/utilities/cursor'; +import positioning from '@styles/utilities/positioning'; +import spacing from '@styles/utilities/spacing'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import {Transaction} from '@src/types/onyx'; -import colors from './colors'; -import fontFamily from './fontFamily'; -import {type ThemeStyles} from './styles'; -import {type ThemeColors} from './themes/types'; -import cursor from './utilities/cursor'; -import positioning from './utilities/positioning'; -import spacing from './utilities/spacing'; -import variables from './variables'; +import createModalStyleUtils from './ModalStyleUtils'; +import createReportActionContextMenuStyleUtils from './ReportActionContextMenuStyleUtils'; +import createTooltipStyleUtils from './TooltipStyleUtils'; type AllStyles = ViewStyle | TextStyle | ImageStyle; type ParsableStyle = StyleProp | ((state: PressableStateCallbackType) => StyleProp); -type AvatarStyle = { - width: number; - height: number; - borderRadius: number; - backgroundColor: string; -}; type ColorValue = ValueOf; type AvatarSizeName = ValueOf; @@ -45,6 +46,14 @@ type AvatarSizeValue = ValueOf< | 'avatarSizeSmallNormal' > >; + +type AvatarStyle = { + width: number; + height: number; + borderRadius: number; + backgroundColor: string; +}; + type ButtonSizeValue = ValueOf; type ButtonStateName = ValueOf; type AvatarSize = {width: number}; @@ -52,36 +61,6 @@ type AvatarSize = {width: number}; type WorkspaceColorStyle = {backgroundColor: ColorValue; fill: ColorValue}; type EreceiptColorStyle = {backgroundColor: ColorValue; color: ColorValue}; -type ModalPaddingStylesParams = { - shouldAddBottomSafeAreaMargin: boolean; - shouldAddTopSafeAreaMargin: boolean; - shouldAddBottomSafeAreaPadding: boolean; - shouldAddTopSafeAreaPadding: boolean; - safeAreaPaddingTop: number; - safeAreaPaddingBottom: number; - safeAreaPaddingLeft: number; - safeAreaPaddingRight: number; - modalContainerStyleMarginTop: DimensionValue | undefined; - modalContainerStyleMarginBottom: DimensionValue | undefined; - modalContainerStylePaddingTop: DimensionValue | undefined; - modalContainerStylePaddingBottom: DimensionValue | undefined; - insets: EdgeInsets; -}; - -type AvatarBorderStyleParams = { - theme: ThemeColors; - isHovered: boolean; - isPressed: boolean; - isInReportAction: boolean; - shouldUseCardBackground: boolean; -}; - -type GetBaseAutoCompleteSuggestionContainerStyleParams = { - left: number; - bottom: number; - width: number; -}; - const workspaceColorOptions: WorkspaceColorStyle[] = [ {backgroundColor: colors.blue200, fill: colors.blue700}, {backgroundColor: colors.blue400, fill: colors.blue800}, @@ -177,39 +156,93 @@ const avatarBorderWidths: Partial> = { }; /** - * Return the style size from an avatar size constant + * Converts a color in hexadecimal notation into RGB notation. + * + * @param hexadecimal A color in hexadecimal notation. + * @returns `undefined` if the input color is not in hexadecimal notation. Otherwise, the RGB components of the input color. */ -function getAvatarSize(size: AvatarSizeName): number { - return avatarSizes[size]; +function hexadecimalToRGBArray(hexadecimal: string): number[] | undefined { + const components = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexadecimal); + + if (components === null) { + return undefined; + } + + return components.slice(1).map((component) => parseInt(component, 16)); +} + +/** + * Converts a color in RGBA notation to an equivalent color in RGB notation. + * + * @param foregroundRGB The three components of the foreground color in RGB notation. + * @param backgroundRGB The three components of the background color in RGB notation. + * @param opacity The desired opacity of the foreground color. + * @returns The RGB components of the RGBA color converted to RGB. + */ +function convertRGBAToRGB(foregroundRGB: number[], backgroundRGB: number[], opacity: number): number[] { + const [foregroundRed, foregroundGreen, foregroundBlue] = foregroundRGB; + const [backgroundRed, backgroundGreen, backgroundBlue] = backgroundRGB; + + return [(1 - opacity) * backgroundRed + opacity * foregroundRed, (1 - opacity) * backgroundGreen + opacity * foregroundGreen, (1 - opacity) * backgroundBlue + opacity * foregroundBlue]; } /** - * Return the height of magic code input container + * Converts three unit values to the three components of a color in RGB notation. + * + * @param red A unit value representing the first component of a color in RGB notation. + * @param green A unit value representing the second component of a color in RGB notation. + * @param blue A unit value representing the third component of a color in RGB notation. + * @returns An array with the three components of a color in RGB notation. */ -function getHeightOfMagicCodeInput(styles: ThemeStyles): ViewStyle { - return {height: styles.magicCodeInputContainer.minHeight - styles.textInputContainer.borderBottomWidth}; +function convertUnitValuesToRGB(red: number, green: number, blue: number): number[] { + return [Math.floor(red * 255), Math.floor(green * 255), Math.floor(blue * 255)]; } /** - * Return the width style from an avatar size constant + * Converts the three components of a color in RGB notation to three unit values. + * + * @param red The first component of a color in RGB notation. + * @param green The second component of a color in RGB notation. + * @param blue The third component of a color in RGB notation. + * @returns An array with three unit values representing the components of a color in RGB notation. */ -function getAvatarWidthStyle(size: AvatarSizeName): ViewStyle { - const avatarSize = getAvatarSize(size); - return { - width: avatarSize, - }; +function convertRGBToUnitValues(red: number, green: number, blue: number): number[] { + return [red / 255, green / 255, blue / 255]; } /** - * Return the style from an avatar size constant + * Matches an RGBA or RGB color value and extracts the color components. + * + * @param color - The RGBA or RGB color value to match and extract components from. + * @returns An array containing the extracted color components [red, green, blue, alpha]. + * + * Returns null if the input string does not match the pattern. */ -function getAvatarStyle(theme: ThemeColors, size: AvatarSizeName): AvatarStyle { +function extractValuesFromRGB(color: string): number[] | null { + const rgbaPattern = /rgba?\((?[.\d]+)[, ]+(?[.\d]+)[, ]+(?[.\d]+)(?:\s?[,/]\s?(?[.\d]+%?))?\)$/i; + const matchRGBA = color.match(rgbaPattern); + if (matchRGBA) { + const [, red, green, blue, alpha] = matchRGBA; + return [parseInt(red, 10), parseInt(green, 10), parseInt(blue, 10), alpha ? parseFloat(alpha) : 1]; + } + + return null; +} + +/** + * Return the style size from an avatar size constant + */ +function getAvatarSize(size: AvatarSizeName): number { + return avatarSizes[size]; +} + +/** + * Return the width style from an avatar size constant + */ +function getAvatarWidthStyle(size: AvatarSizeName): ViewStyle { const avatarSize = getAvatarSize(size); return { - height: avatarSize, width: avatarSize, - borderRadius: avatarSize, - backgroundColor: theme.offline, }; } @@ -307,17 +340,6 @@ function getSafeAreaMargins(insets?: EdgeInsets): ViewStyle { return {marginBottom: (insets?.bottom ?? 0) * variables.safeInsertPercentage}; } -function getZoomCursorStyle(styles: ThemeStyles, isZoomed: boolean, isDragging: boolean): ViewStyle { - if (!isZoomed) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return styles.cursorZoomIn; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return isDragging ? styles.cursorGrabbing : styles.cursorZoomOut; -} - // NOTE: asserting some web style properties to a valid type, because it isn't possible to augment them. function getZoomSizingStyle( isZoomed: boolean, @@ -385,30 +407,6 @@ function getWidthStyle(width: number): ViewStyle { }; } -/** - * Returns auto grow height text input style - */ -function getAutoGrowHeightInputStyle(styles: ThemeStyles, textInputHeight: number, maxHeight: number): ViewStyle { - if (textInputHeight > maxHeight) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...styles.pr0, - ...styles.overflowAuto, - }; - } - - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...styles.pr0, - ...styles.overflowHidden, - // maxHeight is not of the input only but the of the whole input container - // which also includes the top padding and bottom border - height: maxHeight - styles.textInputMultilineContainer.paddingTop - styles.textInputContainer.borderBottomWidth, - }; -} - /** * Returns a style with backgroundColor and borderColor set to the same color */ @@ -462,22 +460,6 @@ function getSignInWordmarkWidthStyle(isSmallScreenWidth: boolean, environment: V return isSmallScreenWidth ? {width: variables.signInLogoWidthPill} : {width: variables.signInLogoWidthLargeScreenPill}; } -/** - * Converts a color in hexadecimal notation into RGB notation. - * - * @param hexadecimal A color in hexadecimal notation. - * @returns `undefined` if the input color is not in hexadecimal notation. Otherwise, the RGB components of the input color. - */ -function hexadecimalToRGBArray(hexadecimal: string): number[] | undefined { - const components = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexadecimal); - - if (components === null) { - return undefined; - } - - return components.slice(1).map((component) => parseInt(component, 16)); -} - /** * Returns a background color with opacity style */ @@ -491,70 +473,6 @@ function getBackgroundColorWithOpacityStyle(backgroundColor: string, opacity: nu return {}; } -/** - * Generate a style for the background color of the Badge - */ -function getBadgeColorStyle(styles: ThemeStyles, isSuccess: boolean, isError: boolean, isPressed = false, isAdHoc = false): ViewStyle { - if (isSuccess) { - if (isAdHoc) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return isPressed ? styles.badgeAdHocSuccessPressed : styles.badgeAdHocSuccess; - } - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return isPressed ? styles.badgeSuccessPressed : styles.badgeSuccess; - } - if (isError) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return isPressed ? styles.badgeDangerPressed : styles.badgeDanger; - } - return {}; -} - -/** - * Generate a style for the background color of the button, based on its current state. - * - * @param buttonState - One of {'default', 'hovered', 'pressed'} - * @param isMenuItem - whether this button is apart of a list - */ -function getButtonBackgroundColorStyle(theme: ThemeColors, buttonState: ButtonStateName = CONST.BUTTON_STATES.DEFAULT, isMenuItem = false): ViewStyle { - switch (buttonState) { - case CONST.BUTTON_STATES.PRESSED: - return {backgroundColor: theme.buttonPressedBG}; - case CONST.BUTTON_STATES.ACTIVE: - return isMenuItem ? {backgroundColor: theme.border} : {backgroundColor: theme.buttonHoveredBG}; - case CONST.BUTTON_STATES.DISABLED: - case CONST.BUTTON_STATES.DEFAULT: - default: - return {}; - } -} - -/** - * Generate fill color of an icon based on its state. - * - * @param buttonState - One of {'default', 'hovered', 'pressed'} - * @param isMenuIcon - whether this icon is apart of a list - */ -function getIconFillColor(theme: ThemeColors, buttonState: ButtonStateName = CONST.BUTTON_STATES.DEFAULT, isMenuIcon = false): string { - switch (buttonState) { - case CONST.BUTTON_STATES.ACTIVE: - case CONST.BUTTON_STATES.PRESSED: - return theme.iconHovered; - case CONST.BUTTON_STATES.COMPLETE: - return theme.iconSuccessFill; - case CONST.BUTTON_STATES.DEFAULT: - case CONST.BUTTON_STATES.DISABLED: - default: - if (isMenuIcon) { - return theme.iconMenu; - } - return theme.icon; - } -} - function getAnimatedFABStyle(rotate: Animated.Value, backgroundColor: Animated.Value): Animated.WithAnimatedValue { return { transform: [{rotate}], @@ -585,6 +503,22 @@ function getCombinedSpacing(modalContainerValue: DimensionValue | undefined, saf return modalContainerValue; } +type ModalPaddingStylesParams = { + shouldAddBottomSafeAreaMargin: boolean; + shouldAddTopSafeAreaMargin: boolean; + shouldAddBottomSafeAreaPadding: boolean; + shouldAddTopSafeAreaPadding: boolean; + safeAreaPaddingTop: number; + safeAreaPaddingBottom: number; + safeAreaPaddingLeft: number; + safeAreaPaddingRight: number; + modalContainerStyleMarginTop: DimensionValue | undefined; + modalContainerStyleMarginBottom: DimensionValue | undefined; + modalContainerStylePaddingTop: DimensionValue | undefined; + modalContainerStylePaddingBottom: DimensionValue | undefined; + insets: EdgeInsets; +}; + function getModalPaddingStyles({ shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaMargin, @@ -645,122 +579,11 @@ function getEmojiPickerStyle(isSmallScreenWidth: boolean): ViewStyle { }; } -/** - * Generate the styles for the ReportActionItem wrapper view. - */ -function getReportActionItemStyle(theme: ThemeColors, styles: ThemeStyles, isHovered = false): ViewStyle { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - display: 'flex', - justifyContent: 'space-between', - backgroundColor: isHovered - ? theme.hoverComponentBG - : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android - theme.transparent, - opacity: 1, - ...styles.cursorInitial, - }; -} - -/** - * Generate the wrapper styles for the mini ReportActionContextMenu. - */ -function getMiniReportActionContextMenuWrapperStyle(styles: ThemeStyles, isReportActionItemGrouped: boolean): ViewStyle { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), - ...positioning.r4, - ...styles.cursorDefault, - position: 'absolute', - zIndex: 8, - }; -} - function getPaymentMethodMenuWidth(isSmallScreenWidth: boolean): ViewStyle { const margin = 20; return {width: !isSmallScreenWidth ? variables.sideBarWidth - margin * 2 : undefined}; } -/** - * Converts a color in RGBA notation to an equivalent color in RGB notation. - * - * @param foregroundRGB The three components of the foreground color in RGB notation. - * @param backgroundRGB The three components of the background color in RGB notation. - * @param opacity The desired opacity of the foreground color. - * @returns The RGB components of the RGBA color converted to RGB. - */ -function convertRGBAToRGB(foregroundRGB: number[], backgroundRGB: number[], opacity: number): number[] { - const [foregroundRed, foregroundGreen, foregroundBlue] = foregroundRGB; - const [backgroundRed, backgroundGreen, backgroundBlue] = backgroundRGB; - - return [(1 - opacity) * backgroundRed + opacity * foregroundRed, (1 - opacity) * backgroundGreen + opacity * foregroundGreen, (1 - opacity) * backgroundBlue + opacity * foregroundBlue]; -} - -/** - * Converts three unit values to the three components of a color in RGB notation. - * - * @param red A unit value representing the first component of a color in RGB notation. - * @param green A unit value representing the second component of a color in RGB notation. - * @param blue A unit value representing the third component of a color in RGB notation. - * @returns An array with the three components of a color in RGB notation. - */ -function convertUnitValuesToRGB(red: number, green: number, blue: number): number[] { - return [Math.floor(red * 255), Math.floor(green * 255), Math.floor(blue * 255)]; -} - -/** - * Converts the three components of a color in RGB notation to three unit values. - * - * @param red The first component of a color in RGB notation. - * @param green The second component of a color in RGB notation. - * @param blue The third component of a color in RGB notation. - * @returns An array with three unit values representing the components of a color in RGB notation. - */ -function convertRGBToUnitValues(red: number, green: number, blue: number): number[] { - return [red / 255, green / 255, blue / 255]; -} - -/** - * Matches an RGBA or RGB color value and extracts the color components. - * - * @param color - The RGBA or RGB color value to match and extract components from. - * @returns An array containing the extracted color components [red, green, blue, alpha]. - * - * Returns null if the input string does not match the pattern. - */ -function extractValuesFromRGB(color: string): number[] | null { - const rgbaPattern = /rgba?\((?[.\d]+)[, ]+(?[.\d]+)[, ]+(?[.\d]+)(?:\s?[,/]\s?(?[.\d]+%?))?\)$/i; - const matchRGBA = color.match(rgbaPattern); - if (matchRGBA) { - const [, red, green, blue, alpha] = matchRGBA; - return [parseInt(red, 10), parseInt(green, 10), parseInt(blue, 10), alpha ? parseFloat(alpha) : 1]; - } - - return null; -} - -/** - * Determines the theme color for a modal based on the app's background color, - * the modal's backdrop, and the backdrop's opacity. - * - * @param bgColor - theme background color - * @returns The theme color as an RGB value. - */ -function getThemeBackgroundColor(theme: ThemeColors, bgColor: string): string { - const backdropOpacity = variables.overlayOpacity; - - const [backgroundRed, backgroundGreen, backgroundBlue] = extractValuesFromRGB(bgColor) ?? hexadecimalToRGBArray(bgColor) ?? []; - const [backdropRed, backdropGreen, backdropBlue] = hexadecimalToRGBArray(theme.overlay) ?? []; - const normalizedBackdropRGB = convertRGBToUnitValues(backdropRed, backdropGreen, backdropBlue); - const normalizedBackgroundRGB = convertRGBToUnitValues(backgroundRed, backgroundGreen, backgroundBlue); - const [red, green, blue] = convertRGBAToRGB(normalizedBackdropRGB, normalizedBackgroundRGB, backdropOpacity); - const themeRGB = convertUnitValuesToRGB(red, green, blue); - - return `rgb(${themeRGB.join(', ')})`; -} - /** * Parse styleParam and return Styles array */ @@ -857,6 +680,14 @@ function fade(fadeAnimation: Animated.Value): Animated.WithAnimatedValue { - return {backgroundColor: isColored ? theme.link : undefined}; -} - -function getEmojiReactionBubbleStyle(theme: ThemeColors, isHovered: boolean, hasUserReacted: boolean, isContextMenu = false): ViewStyle { - let backgroundColor = theme.border; - - if (isHovered) { - backgroundColor = theme.buttonHoveredBG; - } - - if (hasUserReacted) { - backgroundColor = theme.reactionActiveBackground; - } - - if (isContextMenu) { - return { - paddingVertical: 3, - paddingHorizontal: 12, - backgroundColor, - }; - } - - return { - paddingVertical: 2, - paddingHorizontal: 8, - backgroundColor, - }; -} - function getEmojiReactionBubbleTextStyle(isContextMenu = false): TextStyle { if (isContextMenu) { return { @@ -1079,14 +851,6 @@ function getEmojiReactionBubbleTextStyle(isContextMenu = false): TextStyle { }; } -function getEmojiReactionCounterTextStyle(theme: ThemeColors, hasUserReacted: boolean): TextStyle { - if (hasUserReacted) { - return {color: theme.reactionActiveText}; - } - - return {color: theme.text}; -} - /** * Returns a style object with a rotation transformation applied based on the provided direction prop. * @@ -1107,23 +871,6 @@ function displayIfTrue(condition: boolean): ViewStyle { return {display: condition ? 'flex' : 'none'}; } -function getGoogleListViewStyle(styles: ThemeStyles, shouldDisplayBorder: boolean): ViewStyle { - if (shouldDisplayBorder) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...styles.borderTopRounded, - ...styles.borderBottomRounded, - marginTop: 4, - paddingVertical: 6, - }; - } - - return { - transform: 'scale(0)', - }; -} - /** * Gets the correct height for emoji picker list based on screen dimensions */ @@ -1144,25 +891,6 @@ function getEmojiPickerListHeight(hasAdditionalSpace: boolean, windowHeight: num return style; } -/** - * Returns style object for the user mention component based on whether the mention is ours or not. - */ -function getMentionStyle(theme: ThemeColors, isOurMention: boolean): ViewStyle { - const backgroundColor = isOurMention ? theme.ourMentionBG : theme.mentionBG; - return { - backgroundColor, - borderRadius: variables.componentBorderRadiusSmall, - paddingHorizontal: 2, - }; -} - -/** - * Returns text color for the user mention text based on whether the mention is ours or not. - */ -function getMentionTextColor(theme: ThemeColors, isOurMention: boolean): string { - return isOurMention ? theme.ourMentionText : theme.mentionText; -} - /** * Returns padding vertical based on number of lines */ @@ -1209,23 +937,6 @@ function getMenuItemTextContainerStyle(isSmallAvatarSubscriptMenu: boolean): Vie }; } -/** - * Returns link styles based on whether the link is disabled or not - */ -function getDisabledLinkStyles(theme: ThemeColors, styles: ThemeStyles, isDisabled = false): ViewStyle { - const disabledLinkStyles = { - color: theme.textSupporting, - ...cursor.cursorDisabled, - }; - - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...styles.link, - ...(isDisabled ? disabledLinkStyles : {}), - }; -} - /** * Returns color style */ @@ -1246,23 +957,6 @@ function getCheckboxPressableStyle(borderRadius = 6): ViewStyle { }; } -/** - * Returns the checkbox container style - */ -function getCheckboxContainerStyle(theme: ThemeColors, size: number, borderRadius = 4): ViewStyle { - return { - backgroundColor: theme.componentBG, - height: size, - width: size, - borderColor: theme.borderLighter, - borderWidth: 2, - justifyContent: 'center', - alignItems: 'center', - // eslint-disable-next-line object-shorthand - borderRadius: borderRadius, - }; -} - /** * Returns style object for the dropbutton height */ @@ -1337,32 +1031,6 @@ function getAmountFontSizeAndLineHeight(isSmallScreenWidth: boolean, windowWidth }; } -/** - * Returns container styles for showing the icons in MultipleAvatars/SubscriptAvatar - */ -function getContainerStyles(styles: ThemeStyles, size: string, isInReportAction = false): ViewStyle[] { - let containerStyles: ViewStyle[]; - - switch (size) { - case CONST.AVATAR_SIZE.SMALL: - containerStyles = [styles.emptyAvatarSmall, styles.emptyAvatarMarginSmall]; - break; - case CONST.AVATAR_SIZE.SMALLER: - containerStyles = [styles.emptyAvatarSmaller, styles.emptyAvatarMarginSmaller]; - break; - case CONST.AVATAR_SIZE.MEDIUM: - containerStyles = [styles.emptyAvatarMedium, styles.emptyAvatarMargin]; - break; - case CONST.AVATAR_SIZE.LARGE: - containerStyles = [styles.emptyAvatarLarge, styles.mb2, styles.mr2]; - break; - default: - containerStyles = [styles.emptyAvatar, isInReportAction ? styles.emptyAvatarMarginChat : styles.emptyAvatarMargin]; - } - - return containerStyles; -} - /** * Get transparent color by setting alpha value 0 of the passed hex(#xxxxxx) color code */ @@ -1370,35 +1038,21 @@ function getTransparentColor(color: string) { return `${color}00`; } -/** - * Get the styles of the text next to dot indicators - */ -function getDotIndicatorTextStyles(styles: ThemeStyles, isErrorText = true): TextStyle { - return isErrorText ? {...styles.offlineFeedback.text, color: styles.formError.color} : {...styles.offlineFeedback.text}; -} - -export type {AvatarSizeName}; - -export { +const staticStyleUtils = { combineStyles, displayIfTrue, getAmountFontSizeAndLineHeight, getAnimatedFABStyle, getAutoCompleteSuggestionContainerStyle, - getAutoCompleteSuggestionItemStyle, - getAutoGrowHeightInputStyle, getAvatarBorderRadius, getAvatarBorderStyle, getAvatarBorderWidth, getAvatarExtraFontSizeStyle, getAvatarSize, - getAvatarStyle, getAvatarWidthStyle, getBackgroundAndBorderStyle, getBackgroundColorStyle, getBackgroundColorWithOpacityStyle, - getBadgeColorStyle, - getButtonBackgroundColorStyle, getPaddingLeft, hasSafeAreas, getHeight, @@ -1415,51 +1069,389 @@ export { getReportWelcomeContainerStyle, getBaseAutoCompleteSuggestionContainerStyle, getBorderColorStyle, - getCheckboxContainerStyle, getCheckboxPressableStyle, - getColoredBackgroundStyle, getComposeTextAreaPadding, getColorStyle, getDefaultWorkspaceAvatarColor, getDirectionStyle, - getDisabledLinkStyles, getDropDownButtonHeight, - getDotIndicatorTextStyles, getEmojiPickerListHeight, getEmojiPickerStyle, - getEmojiReactionBubbleStyle, getEmojiReactionBubbleTextStyle, - getEmojiReactionCounterTextStyle, - getErrorPageContainerStyle, getFontFamilyMonospace, getCodeFontSize, getFontSizeStyle, - getGoogleListViewStyle, - getHeightOfMagicCodeInput, - getIconFillColor, getLineHeightStyle, - getMentionStyle, - getMentionTextColor, getMenuItemTextContainerStyle, - getMiniReportActionContextMenuWrapperStyle, getModalPaddingStyles, getOuterModalStyle, getPaymentMethodMenuWidth, - getReportActionItemStyle, getSafeAreaMargins, getSafeAreaPadding, getSignInWordmarkWidthStyle, getTextColorStyle, - getThemeBackgroundColor, getTransparentColor, getWidthAndHeightStyle, getWidthStyle, getWrappingStyle, - getZoomCursorStyle, getZoomSizingStyle, parseStyleAsArray, parseStyleFromFunction, - getContainerStyles, getEReceiptColorStyles, getEReceiptColorCode, }; + +const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ + ...staticStyleUtils, + ...createModalStyleUtils(theme, styles), + ...createTooltipStyleUtils(theme, styles), + ...createReportActionContextMenuStyleUtils(theme, styles), + + /** + * Gets styles for AutoCompleteSuggestion row + */ + getAutoCompleteSuggestionItemStyle: (highlightedEmojiIndex: number, rowHeight: number, isHovered: boolean, currentEmojiIndex: number): ViewStyle[] => { + let backgroundColor; + + if (currentEmojiIndex === highlightedEmojiIndex) { + backgroundColor = theme.activeComponentBG; + } else if (isHovered) { + backgroundColor = theme.hoverComponentBG; + } + + return [ + { + height: rowHeight, + justifyContent: 'center', + }, + backgroundColor + ? { + backgroundColor, + } + : {}, + ]; + }, + + /** + * Returns auto grow height text input style + */ + getAutoGrowHeightInputStyle: (textInputHeight: number, maxHeight: number): ViewStyle => { + if (textInputHeight > maxHeight) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...styles.pr0, + ...styles.overflowAuto, + }; + } + + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...styles.pr0, + ...styles.overflowHidden, + // maxHeight is not of the input only but the of the whole input container + // which also includes the top padding and bottom border + height: maxHeight - styles.textInputMultilineContainer.paddingTop - styles.textInputContainer.borderBottomWidth, + }; + }, + + /** + * Return the style from an avatar size constant + */ + getAvatarStyle: (size: AvatarSizeName): AvatarStyle => { + const avatarSize = getAvatarSize(size); + return { + height: avatarSize, + width: avatarSize, + borderRadius: avatarSize, + backgroundColor: theme.offline, + }; + }, + + /** + * Generate a style for the background color of the Badge + */ + getBadgeColorStyle: (isSuccess: boolean, isError: boolean, isPressed = false, isAdHoc = false): ViewStyle => { + if (isSuccess) { + if (isAdHoc) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return isPressed ? styles.badgeAdHocSuccessPressed : styles.badgeAdHocSuccess; + } + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return isPressed ? styles.badgeSuccessPressed : styles.badgeSuccess; + } + if (isError) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return isPressed ? styles.badgeDangerPressed : styles.badgeDanger; + } + return {}; + }, + + /** + * Generate a style for the background color of the button, based on its current state. + * + * @param buttonState - One of {'default', 'hovered', 'pressed'} + * @param isMenuItem - whether this button is apart of a list + */ + getButtonBackgroundColorStyle: (buttonState: ButtonStateName = CONST.BUTTON_STATES.DEFAULT, isMenuItem = false): ViewStyle => { + switch (buttonState) { + case CONST.BUTTON_STATES.PRESSED: + return {backgroundColor: theme.buttonPressedBG}; + case CONST.BUTTON_STATES.ACTIVE: + return isMenuItem ? {backgroundColor: theme.border} : {backgroundColor: theme.buttonHoveredBG}; + case CONST.BUTTON_STATES.DISABLED: + case CONST.BUTTON_STATES.DEFAULT: + default: + return {}; + } + }, + + /** + * Returns the checkbox container style + */ + getCheckboxContainerStyle: (size: number, borderRadius = 4): ViewStyle => ({ + backgroundColor: theme.componentBG, + height: size, + width: size, + borderColor: theme.borderLighter, + borderWidth: 2, + justifyContent: 'center', + alignItems: 'center', + // eslint-disable-next-line object-shorthand + borderRadius: borderRadius, + }), + + /** + * Select the correct color for text. + */ + getColoredBackgroundStyle: (isColored: boolean): StyleProp => ({backgroundColor: isColored ? theme.mentionBG : undefined}), + + /** + * Returns link styles based on whether the link is disabled or not + */ + getDisabledLinkStyles: (isDisabled = false): ViewStyle => { + const disabledLinkStyles = { + color: theme.textSupporting, + ...cursor.cursorDisabled, + }; + + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...styles.link, + ...(isDisabled ? disabledLinkStyles : {}), + }; + }, + + /** + * Get the styles of the text next to dot indicators + */ + getDotIndicatorTextStyles: (isErrorText = true): TextStyle => (isErrorText ? {...styles.offlineFeedback.text, color: styles.formError.color} : {...styles.offlineFeedback.text}), + + getEmojiReactionBubbleStyle: (isHovered: boolean, hasUserReacted: boolean, isContextMenu = false): ViewStyle => { + let backgroundColor = theme.border; + + if (isHovered) { + backgroundColor = theme.buttonHoveredBG; + } + + if (hasUserReacted) { + backgroundColor = theme.reactionActiveBackground; + } + + if (isContextMenu) { + return { + paddingVertical: 3, + paddingHorizontal: 12, + backgroundColor, + }; + } + + return { + paddingVertical: 2, + paddingHorizontal: 8, + backgroundColor, + }; + }, + + getEmojiReactionCounterTextStyle: (hasUserReacted: boolean): TextStyle => { + if (hasUserReacted) { + return {color: theme.reactionActiveText}; + } + + return {color: theme.text}; + }, + + getErrorPageContainerStyle: (safeAreaPaddingBottom = 0): ViewStyle => ({ + backgroundColor: theme.componentBG, + paddingBottom: 40 + safeAreaPaddingBottom, + }), + + getGoogleListViewStyle: (shouldDisplayBorder: boolean): ViewStyle => { + if (shouldDisplayBorder) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...styles.borderTopRounded, + ...styles.borderBottomRounded, + marginTop: 4, + paddingVertical: 6, + }; + } + + return { + transform: 'scale(0)', + }; + }, + + /** + * Return the height of magic code input container + */ + getHeightOfMagicCodeInput: (): ViewStyle => ({height: styles.magicCodeInputContainer.minHeight - styles.textInputContainer.borderBottomWidth}), + + /** + * Generate fill color of an icon based on its state. + * + * @param buttonState - One of {'default', 'hovered', 'pressed'} + * @param isMenuIcon - whether this icon is apart of a list + */ + getIconFillColor: (buttonState: ButtonStateName = CONST.BUTTON_STATES.DEFAULT, isMenuIcon = false): string => { + switch (buttonState) { + case CONST.BUTTON_STATES.ACTIVE: + case CONST.BUTTON_STATES.PRESSED: + return theme.iconHovered; + case CONST.BUTTON_STATES.COMPLETE: + return theme.iconSuccessFill; + case CONST.BUTTON_STATES.DEFAULT: + case CONST.BUTTON_STATES.DISABLED: + default: + if (isMenuIcon) { + return theme.iconMenu; + } + return theme.icon; + } + }, + + /** + * Returns style object for the user mention component based on whether the mention is ours or not. + */ + getMentionStyle: (isOurMention: boolean): ViewStyle => { + const backgroundColor = isOurMention ? theme.ourMentionBG : theme.mentionBG; + return { + backgroundColor, + borderRadius: variables.componentBorderRadiusSmall, + paddingHorizontal: 2, + }; + }, + + /** + * Returns text color for the user mention text based on whether the mention is ours or not. + */ + getMentionTextColor: (isOurMention: boolean): string => (isOurMention ? theme.ourMentionText : theme.mentionText), + + /** + * Generate the wrapper styles for the mini ReportActionContextMenu. + */ + getMiniReportActionContextMenuWrapperStyle: (isReportActionItemGrouped: boolean): ViewStyle => + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + ({ + ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), + ...positioning.r4, + ...styles.cursorDefault, + position: 'absolute', + zIndex: 8, + }), + + /** + * Generate the styles for the ReportActionItem wrapper view. + */ + getReportActionItemStyle: (isHovered = false): ViewStyle => + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + ({ + display: 'flex', + justifyContent: 'space-between', + backgroundColor: isHovered + ? theme.hoverComponentBG + : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android + theme.transparent, + opacity: 1, + ...styles.cursorInitial, + }), + + /** + * Determines the theme color for a modal based on the app's background color, + * the modal's backdrop, and the backdrop's opacity. + * + * @param bgColor - theme background color + * @returns The theme color as an RGB value. + */ + getThemeBackgroundColor: (bgColor: string): string => { + const backdropOpacity = variables.overlayOpacity; + + const [backgroundRed, backgroundGreen, backgroundBlue] = extractValuesFromRGB(bgColor) ?? hexadecimalToRGBArray(bgColor) ?? []; + const [backdropRed, backdropGreen, backdropBlue] = hexadecimalToRGBArray(theme.overlay) ?? []; + const normalizedBackdropRGB = convertRGBToUnitValues(backdropRed, backdropGreen, backdropBlue); + const normalizedBackgroundRGB = convertRGBToUnitValues(backgroundRed, backgroundGreen, backgroundBlue); + const [red, green, blue] = convertRGBAToRGB(normalizedBackdropRGB, normalizedBackgroundRGB, backdropOpacity); + const themeRGB = convertUnitValuesToRGB(red, green, blue); + + return `rgb(${themeRGB.join(', ')})`; + }, + + getZoomCursorStyle: (isZoomed: boolean, isDragging: boolean): ViewStyle => { + if (!isZoomed) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return styles.cursorZoomIn; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return isDragging ? styles.cursorGrabbing : styles.cursorZoomOut; + }, + + /** + * Returns container styles for showing the icons in MultipleAvatars/SubscriptAvatar + */ + getContainerStyles: (size: string, isInReportAction = false): ViewStyle[] => { + let containerStyles: ViewStyle[]; + + switch (size) { + case CONST.AVATAR_SIZE.SMALL: + containerStyles = [styles.emptyAvatarSmall, styles.emptyAvatarMarginSmall]; + break; + case CONST.AVATAR_SIZE.SMALLER: + containerStyles = [styles.emptyAvatarSmaller, styles.emptyAvatarMarginSmaller]; + break; + case CONST.AVATAR_SIZE.MEDIUM: + containerStyles = [styles.emptyAvatarMedium, styles.emptyAvatarMargin]; + break; + case CONST.AVATAR_SIZE.LARGE: + containerStyles = [styles.emptyAvatarLarge, styles.mb2, styles.mr2]; + break; + default: + containerStyles = [styles.emptyAvatar, isInReportAction ? styles.emptyAvatarMarginChat : styles.emptyAvatarMargin]; + } + + return containerStyles; + }, + + getCompactContentContainerStyles: () => compactContentContainerStyles(styles), + + getContextMenuItemStyles: (windowWidth?: number) => getContextMenuItemStyles(styles, windowWidth), + + getContainerComposeStyles: () => containerComposeStyles(styles), +}); + +type StyleUtilsType = ReturnType; + +const DefaultStyleUtils = createStyleUtils(defaultTheme, defaultStyles); + +export default createStyleUtils; +export {DefaultStyleUtils}; +export type {StyleUtilsType, AvatarSizeName}; diff --git a/src/styles/roundToNearestMultipleOfFour.ts b/src/styles/utils/roundToNearestMultipleOfFour.ts similarity index 100% rename from src/styles/roundToNearestMultipleOfFour.ts rename to src/styles/utils/roundToNearestMultipleOfFour.ts diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 18800f5748d9..65d7f6a0311d 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -43,6 +43,7 @@ export default { avatarSizeMentionIcon: 16, avatarSizeSmallSubscript: 14, defaultAvatarPreviewSize: 360, + fabBottom: 25, fontSizeOnlyEmojis: 30, fontSizeOnlyEmojisHeight: 35, fontSizeSmall: getValueUsingPixelRatio(11, 17), diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 7f9957232cfb..78f385cde90b 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -6,7 +6,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails} from '@src/types/onyx'; import Policy from '@src/types/onyx/Policy'; import Report from '@src/types/onyx/Report'; -import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; +import ReportAction from '@src/types/onyx/ReportAction'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; @@ -86,7 +86,7 @@ test('getOrderedReportIDs on 5k reports', async () => { }, ], ]), - ) as unknown as OnyxCollection; + ) as unknown as OnyxCollection; Onyx.multiSet({ ...mockedResponseMap, diff --git a/tests/unit/SidebarFilterTest.js b/tests/unit/SidebarFilterTest.js index 23a958e3aa9d..e421e16e270f 100644 --- a/tests/unit/SidebarFilterTest.js +++ b/tests/unit/SidebarFilterTest.js @@ -13,7 +13,7 @@ jest.mock('../../src/libs/Permissions'); const ONYXKEYS = { PERSONAL_DETAILS_LIST: 'personalDetailsList', - IS_LOADING_REPORT_DATA: 'isLoadingReportData', + IS_LOADING_APP: 'isLoadingApp', NVP_PRIORITY_MODE: 'nvp_priorityMode', SESSION: 'session', BETAS: 'betas', @@ -84,7 +84,7 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, }), ) @@ -113,7 +113,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, }), ) @@ -143,7 +143,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.BETAS]: [], [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -196,7 +196,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.BETAS]: [], [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -248,7 +248,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.BETAS]: [], [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, }), @@ -337,7 +337,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, @@ -381,7 +381,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -452,7 +452,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${draftReport.reportID}`]: draftReport, [`${ONYXKEYS.COLLECTION.REPORT}${pinnedReport.reportID}`]: pinnedReport, }), @@ -500,7 +500,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${archivedReport.reportID}`]: archivedReport, [`${ONYXKEYS.COLLECTION.REPORT}${archivedPolicyRoomReport.reportID}`]: archivedPolicyRoomReport, [`${ONYXKEYS.COLLECTION.REPORT}${archivedUserCreatedPolicyRoomReport.reportID}`]: archivedUserCreatedPolicyRoomReport, @@ -563,7 +563,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${policyRoomReport.reportID}`]: policyRoomReport, [`${ONYXKEYS.COLLECTION.REPORT}${userCreatedPolicyRoomReport.reportID}`]: userCreatedPolicyRoomReport, }), @@ -662,7 +662,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, @@ -714,7 +714,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -765,7 +765,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -814,7 +814,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -859,7 +859,7 @@ describe('Sidebar', () => { [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 6eef3e40bf1c..ecd2afc59a72 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -15,7 +15,7 @@ jest.mock('../../src/components/Icon/Expensicons'); const ONYXKEYS = { PERSONAL_DETAILS_LIST: 'personalDetailsList', - IS_LOADING_REPORT_DATA: 'isLoadingReportData', + IS_LOADING_APP: 'isLoadingApp', NVP_PRIORITY_MODE: 'nvp_priorityMode', SESSION: 'session', BETAS: 'betas', @@ -69,7 +69,7 @@ describe('Sidebar', () => { .then(() => Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, }), ) @@ -94,7 +94,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -126,7 +126,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -172,7 +172,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -215,7 +215,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -443,7 +443,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -488,7 +488,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -525,7 +525,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -587,7 +587,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [ONYXKEYS.SESSION]: {accountID: currentlyLoggedInUserAccountID}, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, @@ -639,7 +639,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -699,7 +699,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -759,7 +759,7 @@ describe('Sidebar', () => { [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -796,7 +796,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -851,7 +851,7 @@ describe('Sidebar', () => { [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, @@ -974,7 +974,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [ONYXKEYS.SESSION]: {accountID: currentlyLoggedInUserAccountID}, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, @@ -1032,7 +1032,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, diff --git a/tests/unit/SidebarTest.js b/tests/unit/SidebarTest.js index 4bd0795aa3b9..106b2c3b69a9 100644 --- a/tests/unit/SidebarTest.js +++ b/tests/unit/SidebarTest.js @@ -13,7 +13,7 @@ jest.mock('../../src/components/Icon/Expensicons'); const ONYXKEYS = { PERSONAL_DETAILS_LIST: 'personalDetailsList', - IS_LOADING_REPORT_DATA: 'isLoadingReportData', + IS_LOADING_APP: 'isLoadingApp', NVP_PRIORITY_MODE: 'nvp_priorityMode', SESSION: 'session', BETAS: 'betas', @@ -66,7 +66,7 @@ describe('Sidebar', () => { [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }), ) @@ -109,7 +109,7 @@ describe('Sidebar', () => { [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionId]: action}, }), diff --git a/tsconfig.json b/tsconfig.json index eafc7c375fdd..08447f306dd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,5 +47,5 @@ } }, "exclude": ["**/node_modules/*", "**/dist/*", ".github/actions/**/index.js", "**/docs/*"], - "include": ["src", "desktop", "web", "docs", "assets", "config", "tests", "jest", "__mocks__", ".github/**/*", ".storybook/**/*"] + "include": ["src", "desktop", "web", "website", "docs", "assets", "config", "tests", "jest", "__mocks__", ".github/**/*", ".storybook/**/*"] }