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/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/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 index 3252938e4ca5..a45aba6ef534 100644 --- a/src/components/BaseMiniContextMenuItem.js +++ b/src/components/BaseMiniContextMenuItem.js @@ -5,8 +5,7 @@ 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 useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; @@ -51,8 +50,8 @@ const defaultProps = { * @returns {JSX.Element} */ function BaseMiniContextMenuItem(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( [ styles.reportActionContextMenuMiniButton, - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(hovered, pressed, props.isDelayButtonStateComplete)), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, props.isDelayButtonStateComplete)), props.isDelayButtonStateComplete && styles.cursorDefault, ]} > 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 ?? ( )} @@ -100,7 +98,7 @@ function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, success={!isThrottledButtonActive} description={description} descriptionTextStyle={styles.breakAll} - style={getContextMenuItemStyles(styles, windowWidth)} + 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..0e3fc7932e46 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(); 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/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 a5b9910842bf..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] diff --git a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js b/src/components/LocationErrorMessage/BaseLocationErrorMessage.js index 4e4912322bb1..a86cb8bd7bd9 100644 --- a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js +++ b/src/components/LocationErrorMessage/BaseLocationErrorMessage.js @@ -9,7 +9,7 @@ import TextLink from '@components/TextLink'; import Tooltip from '@components/Tooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import colors from '@styles/colors'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes'; @@ -27,6 +27,7 @@ const propTypes = { function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationErrorCode, translate}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); if (!locationErrorCode) { return null; } @@ -44,14 +45,14 @@ function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationEr {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/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.tsx b/src/components/OfflineWithFeedback.tsx index 17772e148f53..78c93c250f84 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +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 * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -56,27 +56,6 @@ type OfflineWithFeedbackProps = ChildrenProps & { type StrikethroughProps = Partial & {style: Array}; -/** - * This method applies the strikethrough to all the children passed recursively - */ -function applyStrikeThrough(children: React.ReactNode, styles: ReturnType): React.ReactNode { - return React.Children.map(children, (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, styles); - } - - return React.cloneElement(child, props); - }); -} - 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))); @@ -98,6 +77,7 @@ function OfflineWithFeedback({ ...rest }: OfflineWithFeedbackProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {isOffline} = useNetwork(); const hasErrors = isNotEmptyObject(errors ?? {}); @@ -112,9 +92,32 @@ function OfflineWithFeedback({ 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, styles); + children = applyStrikeThrough(children); } return ( 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 4e94a9afe018..8dcdb32e530a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -28,8 +28,8 @@ 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'; @@ -140,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) { 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..ea05b78f98a7 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; } @@ -64,7 +65,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 ( 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..6067d21803ac 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({ 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/TextInput/BaseTextInput/index.js b/src/components/TextInput/BaseTextInput/index.js index cf8ae90c727a..9c9afb6ebacc 100644 --- a/src/components/TextInput/BaseTextInput/index.js +++ b/src/components/TextInput/BaseTextInput/index.js @@ -16,8 +16,8 @@ import withLocalize from '@components/withLocalize'; import * as Browser from '@libs/Browser'; 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); @@ -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/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/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/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/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/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/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 a3fdc7f574f6..f99bd7ab7d9d 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/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/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/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/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/styles.ts b/src/styles/styles.ts index ef47b1c7faa6..2d1a8b99a345 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'; @@ -3997,8 +3997,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/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 0ebc7e1d583f..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.mentionBG : 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