diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index ee800c400a93..feeb64856a8c 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -109,8 +109,8 @@ function BlockingView({ CustomSubtitle, contentFitImage, containerStyle: containerStyleProp, - addBottomSafeAreaPadding = false, - addOfflineIndicatorBottomSafeAreaPadding = addBottomSafeAreaPadding, + addBottomSafeAreaPadding, + addOfflineIndicatorBottomSafeAreaPadding, testID, }: BlockingViewProps) { const styles = useThemeStyles(); diff --git a/src/components/CategorySelector/CategorySelectorModal.tsx b/src/components/CategorySelector/CategorySelectorModal.tsx index 4bcfb9e1f9b6..6f2637eeb8e9 100644 --- a/src/components/CategorySelector/CategorySelectorModal.tsx +++ b/src/components/CategorySelector/CategorySelectorModal.tsx @@ -56,7 +56,6 @@ function CategorySelectorModal({policyID, isVisible, currentCategory, onCategory policyID={policyID} selectedCategory={currentCategory} onSubmit={onCategorySelected} - contentContainerStyle={styles.pb5} addBottomSafeAreaPadding /> diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index 2a17997324f3..c5910647c705 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -22,13 +22,7 @@ type FixedFooterProps = { shouldStickToBottom?: boolean; }; -function FixedFooter({ - style, - children, - addBottomSafeAreaPadding = false, - addOfflineIndicatorBottomSafeAreaPadding = addBottomSafeAreaPadding, - shouldStickToBottom = false, -}: FixedFooterProps) { +function FixedFooter({style, children, addBottomSafeAreaPadding, addOfflineIndicatorBottomSafeAreaPadding, shouldStickToBottom = false}: FixedFooterProps) { const styles = useThemeStyles(); const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({ diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 3120c5a3fffb..c2730c1e5ea7 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -83,7 +83,7 @@ function FormWrapper({ isLoading = false, shouldScrollToEnd = false, addBottomSafeAreaPadding, - addOfflineIndicatorBottomSafeAreaPadding: addOfflineIndicatorBottomSafeAreaPaddingProp, + addOfflineIndicatorBottomSafeAreaPadding, shouldSubmitButtonStickToBottom: shouldSubmitButtonStickToBottomProp, shouldSubmitButtonBlendOpacity = false, }: FormWrapperProps) { @@ -137,13 +137,13 @@ function FormWrapper({ // If the paddingBottom is 0, it has already been applied to a parent component and we don't want to apply the padding again. const isLegacyBottomSafeAreaPaddingAlreadyApplied = paddingBottom === 0; const shouldApplyBottomSafeAreaPadding = addBottomSafeAreaPadding ?? !isLegacyBottomSafeAreaPaddingAlreadyApplied; - const addOfflineIndicatorBottomSafeAreaPadding = addOfflineIndicatorBottomSafeAreaPaddingProp ?? addBottomSafeAreaPadding === true; // We need to add bottom safe area padding to the submit button when we don't use a scroll view or // when the submit button is sticking to the bottom. const addSubmitButtonBottomSafeAreaPadding = addBottomSafeAreaPadding && (!shouldUseScrollView || shouldSubmitButtonStickToBottom); const submitButtonStylesWithBottomSafeAreaPadding = useBottomSafeSafeAreaPaddingStyle({ addBottomSafeAreaPadding: addSubmitButtonBottomSafeAreaPadding, + addOfflineIndicatorBottomSafeAreaPadding, styleProperty: shouldSubmitButtonStickToBottom ? 'bottom' : 'paddingBottom', additionalPaddingBottom: shouldSubmitButtonStickToBottom ? styles.pb5.paddingBottom : 0, style: submitButtonStyles, diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index fd44f593f189..b65040864272 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {forwardRef, useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {ModalProps as ReactNativeModalProps} from 'react-native-modal'; import ReactNativeModal from 'react-native-modal'; @@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import NavigationBar from '@components/NavigationBar'; +import ScreenWrapperOfflineIndicatorContext from '@components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext'; import useKeyboardState from '@hooks/useKeyboardState'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -16,6 +17,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ComposerFocusManager from '@libs/ComposerFocusManager'; +import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPaneContext'; import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; @@ -249,76 +251,84 @@ function BaseModal( [isVisible, type], ); + // In Modals we need to reset the ScreenWrapperOfflineIndicatorContext to allow nested ScreenWrapper components to render offline indicators, + // except if we are in a narrow pane navigator. In this case, we use the narrow pane's original values. + const {isInNarrowPane} = useContext(NarrowPaneContext); + const {originalValues} = useContext(ScreenWrapperOfflineIndicatorContext); + const offlineIndicatorContextValue = useMemo(() => (isInNarrowPane ? originalValues ?? {} : {}), [isInNarrowPane, originalValues]); + return ( - - e.stopPropagation()} - onBackdropPress={handleBackdropPress} - // Note: Escape key on web/desktop will trigger onBackButtonPress callback - // eslint-disable-next-line react/jsx-props-no-multi-spaces - onBackButtonPress={closeTop} - onModalShow={handleShowModal} - propagateSwipe={propagateSwipe} - onModalHide={hideModal} - onModalWillShow={saveFocusState} - onDismiss={handleDismissModal} - onSwipeComplete={() => onClose?.()} - swipeDirection={swipeDirection} - swipeThreshold={swipeThreshold} - isVisible={isVisible} - backdropColor={theme.overlay} - backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity} - backdropTransitionOutTiming={0} - hasBackdrop={fullscreen} - coverScreen={fullscreen} - style={[modalStyle, sidePanelStyle]} - deviceHeight={windowHeight} - deviceWidth={windowWidth} - animationIn={animationIn ?? modalStyleAnimationIn} - animationInDelay={animationInDelay} - animationOut={animationOut ?? modalStyleAnimationOut} - useNativeDriver={useNativeDriver} - useNativeDriverForBackdrop={useNativeDriverForBackdrop} - hideModalContentWhileAnimating={hideModalContentWhileAnimating} - animationInTiming={animationInTiming} - animationOutTiming={animationOutTiming} - statusBarTranslucent={statusBarTranslucent} - navigationBarTranslucent={navigationBarTranslucent} - onLayout={onLayout} - avoidKeyboard={avoidKeyboard} - customBackdrop={shouldUseCustomBackdrop ? : undefined} - type={type} - shouldUseNewModal={shouldUseNewModal} + + - e.stopPropagation()} + onBackdropPress={handleBackdropPress} + // Note: Escape key on web/desktop will trigger onBackButtonPress callback + // eslint-disable-next-line react/jsx-props-no-multi-spaces + onBackButtonPress={closeTop} + onModalShow={handleShowModal} + propagateSwipe={propagateSwipe} + onModalHide={hideModal} onModalWillShow={saveFocusState} onDismiss={handleDismissModal} + onSwipeComplete={() => onClose?.()} + swipeDirection={swipeDirection} + swipeThreshold={swipeThreshold} + isVisible={isVisible} + backdropColor={theme.overlay} + backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity} + backdropTransitionOutTiming={0} + hasBackdrop={fullscreen} + coverScreen={fullscreen} + style={[modalStyle, sidePanelStyle]} + deviceHeight={windowHeight} + deviceWidth={windowWidth} + animationIn={animationIn ?? modalStyleAnimationIn} + animationInDelay={animationInDelay} + animationOut={animationOut ?? modalStyleAnimationOut} + useNativeDriver={useNativeDriver} + useNativeDriverForBackdrop={useNativeDriverForBackdrop} + hideModalContentWhileAnimating={hideModalContentWhileAnimating} + animationInTiming={animationInTiming} + animationOutTiming={animationOutTiming} + statusBarTranslucent={statusBarTranslucent} + navigationBarTranslucent={navigationBarTranslucent} + onLayout={onLayout} + avoidKeyboard={avoidKeyboard} + customBackdrop={shouldUseCustomBackdrop ? : undefined} + type={type} + shouldUseNewModal={shouldUseNewModal} > - - - {children} - - - - {!keyboardStateContextValue?.isKeyboardActive && } - - + + {children} + + + + {!keyboardStateContextValue?.isKeyboardActive && } + + + ); } diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx index db5250ee8234..ea9f3aff1fac 100644 --- a/src/components/OfflineIndicator.tsx +++ b/src/components/OfflineIndicator.tsx @@ -31,6 +31,7 @@ function OfflineIndicator({style, containerStyles: containerStylesProp, addBotto const fallbackStyle = useMemo(() => [styles.offlineIndicatorContainer, containerStylesProp], [styles.offlineIndicatorContainer, containerStylesProp]); const containerStyles = useBottomSafeSafeAreaPaddingStyle({ addBottomSafeAreaPadding, + addOfflineIndicatorBottomSafeAreaPadding: false, style: fallbackStyle, }); diff --git a/src/components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext.ts b/src/components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext.ts new file mode 100644 index 000000000000..15cdfa5e9e49 --- /dev/null +++ b/src/components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext.ts @@ -0,0 +1,19 @@ +import {createContext} from 'react'; + +type ScreenWrapperOfflineIndicatorBaseContext = { + showOnSmallScreens?: boolean; + showOnWideScreens?: boolean; + addSafeAreaPadding?: boolean; +}; + +type ScreenWrapperOfflineIndicatorContextType = { + showOnSmallScreens?: boolean; + showOnWideScreens?: boolean; + addSafeAreaPadding?: boolean; + originalValues?: ScreenWrapperOfflineIndicatorBaseContext; +}; + +const ScreenWrapperOfflineIndicatorContext = createContext({}); + +export default ScreenWrapperOfflineIndicatorContext; +export type {ScreenWrapperOfflineIndicatorContextType}; diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper/index.tsx similarity index 72% rename from src/components/ScreenWrapper.tsx rename to src/components/ScreenWrapper/index.tsx index 03b3489048f8..a2ec206b2e1f 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper/index.tsx @@ -7,6 +7,18 @@ import {Keyboard, PanResponder, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import {PickerAvoidingView} from 'react-native-picker-select'; import type {EdgeInsets} from 'react-native-safe-area-context'; +import CustomDevMenu from '@components/CustomDevMenu'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import type FocusTrapForScreenProps from '@components/FocusTrap/FocusTrapForScreen/FocusTrapProps'; +import HeaderGap from '@components/HeaderGap'; +import ImportedStateIndicator from '@components/ImportedStateIndicator'; +import {InitialURLContext} from '@components/InitialURLContextProvider'; +import {useInputBlurContext} from '@components/InputBlurContext'; +import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; +import ModalContext from '@components/Modal/ModalContext'; +import OfflineIndicator from '@components/OfflineIndicator'; +import withNavigationFallback from '@components/withNavigationFallback'; import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; @@ -18,6 +30,7 @@ import useTackInputFocus from '@hooks/useTackInputFocus'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {isMobile, isMobileWebKit, isSafari} from '@libs/Browser'; +import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPaneContext'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} from '@libs/Navigation/types'; @@ -26,18 +39,7 @@ import toggleTestToolsModal from '@userActions/TestTool'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import CustomDevMenu from './CustomDevMenu'; -import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; -import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen'; -import type FocusTrapForScreenProps from './FocusTrap/FocusTrapForScreen/FocusTrapProps'; -import HeaderGap from './HeaderGap'; -import ImportedStateIndicator from './ImportedStateIndicator'; -import {InitialURLContext} from './InitialURLContextProvider'; -import {useInputBlurContext} from './InputBlurContext'; -import KeyboardAvoidingView from './KeyboardAvoidingView'; -import ModalContext from './Modal/ModalContext'; -import OfflineIndicator from './OfflineIndicator'; -import withNavigationFallback from './withNavigationFallback'; +import ScreenWrapperOfflineIndicatorContext from './ScreenWrapperOfflineIndicatorContext'; type ScreenWrapperChildrenProps = { insets: EdgeInsets; @@ -98,9 +100,15 @@ type ScreenWrapperProps = { /** Whether to use the minHeight. Use true for screens where the window height are changing because of Virtual Keyboard */ shouldEnableMinHeight?: boolean; - /** Whether to show offline indicator */ + /** Whether to disable the safe area padding for (nested) offline indicators */ + disableOfflineIndicatorSafeAreaPadding?: boolean; + + /** Whether to show offline indicator on small screens */ shouldShowOfflineIndicator?: boolean; + /** Whether to show offline indicator on wide screens */ + shouldShowOfflineIndicatorInWideScreen?: boolean; + /** Whether to avoid scroll on virtual viewport */ shouldAvoidScrollOnVirtualViewport?: boolean; @@ -115,9 +123,6 @@ type ScreenWrapperProps = { */ navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp; - /** Whether to show offline indicator on wide screens */ - shouldShowOfflineIndicatorInWideScreen?: boolean; - /** Overrides the focus trap default settings */ focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings']; @@ -161,7 +166,9 @@ function ScreenWrapper( shouldEnablePickerAvoiding = true, headerGapStyles, children, - shouldShowOfflineIndicator = true, + disableOfflineIndicatorSafeAreaPadding, + shouldShowOfflineIndicatorInWideScreen, + shouldShowOfflineIndicator, offlineIndicatorStyle, style, shouldDismissKeyboardBeforeClose = true, @@ -169,7 +176,6 @@ function ScreenWrapper( testID, navigation: navigationProp, shouldAvoidScrollOnVirtualViewport = true, - shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, bottomContent, @@ -333,7 +339,7 @@ function ScreenWrapper( }, [isUsingEdgeToEdgeMode, ignoreInsetsConsumption, includePaddingTop, paddingTop, unmodifiedPaddings.top]); const showBottomContent = isUsingEdgeToEdgeMode ? !!bottomContent : true; - const edgeToEdgeBottomContentStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true}); + const edgeToEdgeBottomContentStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true, addOfflineIndicatorBottomSafeAreaPadding: false}); const legacyBottomContentStyle: StyleProp = useMemo(() => { const shouldUseUnmodifiedPaddings = includeSafeAreaPaddingBottom && ignoreInsetsConsumption; if (shouldUseUnmodifiedPaddings) { @@ -360,7 +366,7 @@ function ScreenWrapper( * By default, the background color of the mobile offline indicator is opaque. * If `isOfflineIndicatorTranslucent` is set to true, a translucent background color is applied. */ - const mobileOfflineIndicatorBackgroundStyle = useMemo(() => { + const smallScreenOfflineIndicatorBackgroundStyle = useMemo(() => { const showOfflineIndicatorBackground = !bottomContent && isOffline; if (!showOfflineIndicatorBackground) { return undefined; @@ -369,7 +375,7 @@ function ScreenWrapper( }, [bottomContent, isOffline, isOfflineIndicatorTranslucent, styles.appBG, styles.translucentNavigationBarBG]); /** In edge-to-edge mode, we always want to apply the bottom safe area padding to the mobile offline indicator. */ - const hasMobileOfflineIndicatorBottomSafeAreaPadding = isUsingEdgeToEdgeMode ? enableEdgeToEdgeBottomSafeAreaPadding : !includeSafeAreaPaddingBottom; + const hasSmallScreenOfflineIndicatorBottomSafeAreaPadding = isUsingEdgeToEdgeMode ? enableEdgeToEdgeBottomSafeAreaPadding : !includeSafeAreaPaddingBottom; /** * This style includes the bottom safe area padding for the mobile offline indicator. @@ -379,42 +385,73 @@ function ScreenWrapper( * two overlapping layers of translucent background. * If the device does not have soft keys, the bottom safe area padding is applied as `paddingBottom`. */ - const mobileOfflineIndicatorBottomSafeAreaStyle = useBottomSafeSafeAreaPaddingStyle({ - addBottomSafeAreaPadding: hasMobileOfflineIndicatorBottomSafeAreaPadding, + const smallScreenOfflineIndicatorBottomSafeAreaStyle = useBottomSafeSafeAreaPaddingStyle({ + addBottomSafeAreaPadding: hasSmallScreenOfflineIndicatorBottomSafeAreaPadding, + addOfflineIndicatorBottomSafeAreaPadding: false, styleProperty: isSoftKeyNavigation ? 'bottom' : 'paddingBottom', }); /** If there is no bottom content, the mobile offline indicator will stick to the bottom of the screen by default. */ - const displayStickyMobileOfflineIndicator = shouldMobileOfflineIndicatorStickToBottom && !bottomContent; + const displayStickySmallScreenOfflineIndicator = shouldMobileOfflineIndicatorStickToBottom && !bottomContent; /** - * This style includes all styles applied to the container of the mobile offline indicator. + * This style includes all styles applied to the container of the offline indicator on small screens. * It always applies the bottom safe area padding as well as the background style, if the device has soft keys. * In this case, we want the whole container (including the bottom safe area padding) to have translucent/opaque background. */ - const mobileOfflineIndicatorContainerStyle = useMemo( - () => [mobileOfflineIndicatorBottomSafeAreaStyle, displayStickyMobileOfflineIndicator && styles.stickToBottom, !isSoftKeyNavigation && mobileOfflineIndicatorBackgroundStyle], - [mobileOfflineIndicatorBottomSafeAreaStyle, displayStickyMobileOfflineIndicator, styles.stickToBottom, isSoftKeyNavigation, mobileOfflineIndicatorBackgroundStyle], + const smallScreenOfflineIndicatorContainerStyle = useMemo( + () => [ + smallScreenOfflineIndicatorBottomSafeAreaStyle, + displayStickySmallScreenOfflineIndicator && styles.stickToBottom, + !isSoftKeyNavigation && smallScreenOfflineIndicatorBackgroundStyle, + ], + [smallScreenOfflineIndicatorBottomSafeAreaStyle, displayStickySmallScreenOfflineIndicator, styles.stickToBottom, isSoftKeyNavigation, smallScreenOfflineIndicatorBackgroundStyle], ); /** - * This style includes the styles applied to the mobile offline indicator component. - * If the device has soft keys, we only want to apply the background style to the mobile offline indicator component, + * This style includes the styles applied to the offline indicator component on small screens. + * If the device has soft keys, we only want to apply the background style to the offline indicator component, * rather than the whole container, because otherwise the navigation bar would be extra opaque, since it already has a translucent background. */ - const mobileOfflineIndicatorStyle = useMemo( - () => [styles.pl5, isSoftKeyNavigation && mobileOfflineIndicatorBackgroundStyle, offlineIndicatorStyle], - [isSoftKeyNavigation, mobileOfflineIndicatorBackgroundStyle, offlineIndicatorStyle, styles.pl5], + const smallScreenOfflineIndicatorStyle = useMemo( + () => [styles.pl5, isSoftKeyNavigation && smallScreenOfflineIndicatorBackgroundStyle, offlineIndicatorStyle], + [isSoftKeyNavigation, smallScreenOfflineIndicatorBackgroundStyle, offlineIndicatorStyle, styles.pl5], ); - const displayMobileOfflineIndicator = isSmallScreenWidth && shouldShowOfflineIndicator; - const displayWidescreenOfflineIndicator = !shouldUseNarrowLayout && shouldShowOfflineIndicatorInWideScreen; + // This context allows us to disable the safe area padding offseting the offline indicator in scrollable components like 'ScrollView', 'SelectionList' or 'FormProvider'. + // This is useful e.g. for the RightModalNavigator, where we want to avoid the safe area padding offseting the offline indicator because we only show the offline indicator on small screens. + const {isInNarrowPane} = useContext(NarrowPaneContext); + const {addSafeAreaPadding, showOnSmallScreens, showOnWideScreens, originalValues} = useContext(ScreenWrapperOfflineIndicatorContext); + const offlineIndicatorContextValue = useMemo(() => { + const newAddSafeAreaPadding = isInNarrowPane ? isSmallScreenWidth : addSafeAreaPadding; + + const newOriginalValues = originalValues ?? { + addSafeAreaPadding: newAddSafeAreaPadding, + showOnSmallScreens, + showOnWideScreens, + }; + + return { + // Allows for individual screens to disable the offline indicator safe area padding for the screen and all nested ScreenWrapper components. + addSafeAreaPadding: disableOfflineIndicatorSafeAreaPadding === undefined ? newAddSafeAreaPadding ?? true : !disableOfflineIndicatorSafeAreaPadding, + // Prevent any nested ScreenWrapper components from rendering another offline indicator. + showOnSmallScreens: false, + showOnWideScreens: false, + // Pass down the original values by the outermost ScreenWrapperOfflineIndicatorContext.Provider, + // to allow nested ScreenWrapperOfflineIndicatorContext.Provider to access these values. (e.g. in Modals) + originalValues: newOriginalValues, + }; + }, [addSafeAreaPadding, disableOfflineIndicatorSafeAreaPadding, isInNarrowPane, isSmallScreenWidth, originalValues, showOnSmallScreens, showOnWideScreens]); + + const displaySmallScreenOfflineIndicator = isSmallScreenWidth && (shouldShowOfflineIndicator ?? showOnSmallScreens ?? true); + const displayWidescreenOfflineIndicator = !shouldUseNarrowLayout && (shouldShowOfflineIndicatorInWideScreen ?? showOnWideScreens ?? false); /** If we currently show the offline indicator and it has bottom safe area padding, we need to offset the bottom safe area padding in the KeyboardAvoidingView. */ - const shouldOffsetMobileOfflineIndicator = displayMobileOfflineIndicator && hasMobileOfflineIndicatorBottomSafeAreaPadding && isOffline; + const shouldOffsetMobileOfflineIndicator = displaySmallScreenOfflineIndicator && hasSmallScreenOfflineIndicatorBottomSafeAreaPadding && isOffline; const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && isMobileWebKit()); - const contextValue = useMemo( + + const statusContextValue = useMemo( () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied: includeSafeAreaPaddingBottom}), [didScreenTransitionEnd, includeSafeAreaPaddingBottom, isSafeAreaTopPaddingApplied], ); @@ -449,38 +486,40 @@ function ScreenWrapper( > {isDevelopment && } - - { - // If props.children is a function, call it to provide the insets to the children. - typeof children === 'function' - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children - } - {displayMobileOfflineIndicator && ( - <> - {isOffline && ( - - - {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} - - )} - - - )} - {displayWidescreenOfflineIndicator && ( - <> - - {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} - - - )} + + + { + // If props.children is a function, call it to provide the insets to the children. + typeof children === 'function' + ? children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd, + }) + : children + } + {displaySmallScreenOfflineIndicator && ( + <> + {isOffline && ( + + + {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} + + )} + + + )} + {displayWidescreenOfflineIndicator && ( + <> + + {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} + + + )} + diff --git a/src/components/ScrollView.tsx b/src/components/ScrollView.tsx index 660f43114ca6..16fd2dd167ea 100644 --- a/src/components/ScrollView.tsx +++ b/src/components/ScrollView.tsx @@ -14,14 +14,7 @@ type ScrollViewProps = RNScrollViewProps & { }; function ScrollView( - { - children, - scrollIndicatorInsets, - contentContainerStyle: contentContainerStyleProp, - addBottomSafeAreaPadding = false, - addOfflineIndicatorBottomSafeAreaPadding = addBottomSafeAreaPadding, - ...props - }: ScrollViewProps, + {children, scrollIndicatorInsets, contentContainerStyle: contentContainerStyleProp, addBottomSafeAreaPadding, addOfflineIndicatorBottomSafeAreaPadding, ...props}: ScrollViewProps, ref: ForwardedRef, ) { const contentContainerStyle = useBottomSafeSafeAreaPaddingStyle({ diff --git a/src/components/SectionList/BaseSectionList.tsx b/src/components/SectionList/BaseSectionList.tsx index 18a915d1a58b..eb39de8bd541 100644 --- a/src/components/SectionList/BaseSectionList.tsx +++ b/src/components/SectionList/BaseSectionList.tsx @@ -4,12 +4,7 @@ import AnimatedSectionList from './AnimatedSectionList'; import type {SectionListProps, SectionListRef} from './types'; function BaseSectionList( - { - addBottomSafeAreaPadding = false, - addOfflineIndicatorBottomSafeAreaPadding = addBottomSafeAreaPadding, - contentContainerStyle: contentContainerStyleProp, - ...restProps - }: SectionListProps, + {addBottomSafeAreaPadding, addOfflineIndicatorBottomSafeAreaPadding, contentContainerStyle: contentContainerStyleProp, ...restProps}: SectionListProps, ref: SectionListRef, ) { const contentContainerStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding, addOfflineIndicatorBottomSafeAreaPadding, style: contentContainerStyleProp}); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index c516762fc044..60e22bfc6022 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -135,8 +135,8 @@ function BaseSelectionList( listItemTitleContainerStyles, isScreenFocused = false, shouldSubscribeToArrowKeyEvents = true, - addBottomSafeAreaPadding = false, - addOfflineIndicatorBottomSafeAreaPadding = addBottomSafeAreaPadding, + addBottomSafeAreaPadding, + addOfflineIndicatorBottomSafeAreaPadding, fixedNumItemsForLoader, loaderSpeed, errorText, diff --git a/src/components/ValuePicker/ValueSelectionList.tsx b/src/components/ValuePicker/ValueSelectionList.tsx index 9ba6ea564baa..cad4dda3e36b 100644 --- a/src/components/ValuePicker/ValueSelectionList.tsx +++ b/src/components/ValuePicker/ValueSelectionList.tsx @@ -18,6 +18,7 @@ function ValueSelectionList({items = [], selectedItem, onItemSelected, shouldSho shouldShowTooltips={shouldShowTooltips} shouldUpdateFocusedIndex ListItem={RadioListItem} + addBottomSafeAreaPadding /> ); } diff --git a/src/components/ValuePicker/ValueSelectorModal.tsx b/src/components/ValuePicker/ValueSelectorModal.tsx index fdbb8b322aef..9e08ce7e4b34 100644 --- a/src/components/ValuePicker/ValueSelectorModal.tsx +++ b/src/components/ValuePicker/ValueSelectorModal.tsx @@ -2,7 +2,6 @@ import React from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; -import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {ValueSelectorModalProps} from './types'; import ValueSelectionList from './ValueSelectionList'; @@ -18,8 +17,6 @@ function ValueSelectorModal({ onBackdropPress, shouldEnableKeyboardAvoidingView = true, }: ValueSelectorModalProps) { - const styles = useThemeStyles(); - return ( diff --git a/src/hooks/useBottomSafeSafeAreaPaddingStyle.ts b/src/hooks/useBottomSafeSafeAreaPaddingStyle.ts index e680af8a73bd..0b2912ab5470 100644 --- a/src/hooks/useBottomSafeSafeAreaPaddingStyle.ts +++ b/src/hooks/useBottomSafeSafeAreaPaddingStyle.ts @@ -1,6 +1,7 @@ -import {useMemo} from 'react'; +import {useContext, useMemo} from 'react'; import {StyleSheet} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; +import ScreenWrapperOfflineIndicatorContext from '@components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext'; import CONST from '@src/CONST'; import useNetwork from './useNetwork'; import useSafeAreaPaddings from './useSafeAreaPaddings'; @@ -29,11 +30,16 @@ type UseBottomSafeAreaPaddingStyleParams = { * @param params - The parameters for the hook. * @returns The style with bottom safe area padding applied. */ -function useBottomSafeSafeAreaPaddingStyle(params?: UseBottomSafeAreaPaddingStyleParams) { +function useBottomSafeSafeAreaPaddingStyle({ + addBottomSafeAreaPadding = false, + addOfflineIndicatorBottomSafeAreaPadding = addBottomSafeAreaPadding, + style, + styleProperty = 'paddingBottom', + additionalPaddingBottom = 0, +}: UseBottomSafeAreaPaddingStyleParams = {}) { const {paddingBottom: safeAreaPaddingBottom} = useSafeAreaPaddings(true); const {isOffline} = useNetwork(); - - const {addBottomSafeAreaPadding, addOfflineIndicatorBottomSafeAreaPadding, style, styleProperty = 'paddingBottom', additionalPaddingBottom = 0} = params ?? {}; + const {addSafeAreaPadding: isOfflineIndicatorSafeAreaPaddingEnabled} = useContext(ScreenWrapperOfflineIndicatorContext); return useMemo>(() => { let totalPaddingBottom: number | string = additionalPaddingBottom; @@ -43,7 +49,7 @@ function useBottomSafeSafeAreaPaddingStyle(params?: UseBottomSafeAreaPaddingStyl totalPaddingBottom += safeAreaPaddingBottom; } - if (addOfflineIndicatorBottomSafeAreaPadding && isOffline) { + if (addOfflineIndicatorBottomSafeAreaPadding && isOffline && isOfflineIndicatorSafeAreaPaddingEnabled) { totalPaddingBottom += CONST.OFFLINE_INDICATOR_HEIGHT; } @@ -71,7 +77,16 @@ function useBottomSafeSafeAreaPaddingStyle(params?: UseBottomSafeAreaPaddingStyl // If no style is provided, return the padding as an object return {paddingBottom: totalPaddingBottom}; - }, [additionalPaddingBottom, addBottomSafeAreaPadding, addOfflineIndicatorBottomSafeAreaPadding, isOffline, style, safeAreaPaddingBottom, styleProperty]); + }, [ + additionalPaddingBottom, + addBottomSafeAreaPadding, + addOfflineIndicatorBottomSafeAreaPadding, + isOffline, + isOfflineIndicatorSafeAreaPaddingEnabled, + style, + safeAreaPaddingBottom, + styleProperty, + ]); } export default useBottomSafeSafeAreaPaddingStyle; diff --git a/src/hooks/useScreenWrapperTransitionStatus.ts b/src/hooks/useScreenWrapperTransitionStatus.ts index 0d58bf3120c8..de94c9708797 100644 --- a/src/hooks/useScreenWrapperTransitionStatus.ts +++ b/src/hooks/useScreenWrapperTransitionStatus.ts @@ -7,11 +7,11 @@ import {ScreenWrapperStatusContext} from '@components/ScreenWrapper'; * @returns `didScreenTransitionEnd` flag to indicate if navigation transition ended. */ export default function useScreenWrapperTransitionStatus() { - const value = useContext(ScreenWrapperStatusContext); + const context = useContext(ScreenWrapperStatusContext); - if (value === undefined) { + if (context === undefined) { throw new Error("Couldn't find values for screen ScreenWrapper transition status. Are you inside a screen in ScreenWrapper?"); } - return value; + return {didScreenTransitionEnd: context.didScreenTransitionEnd}; } diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx index aaf40a62f99e..1fb7c1ea44b3 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx @@ -10,6 +10,7 @@ import type {AuthScreensParamList, LeftModalNavigatorParamList} from '@libs/Navi import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; +import {NarrowPaneContextProvider} from './NarrowPaneContext'; import Overlay from './Overlay'; type LeftModalNavigatorProps = PlatformStackScreenProps; @@ -24,25 +25,27 @@ function LeftModalNavigator({navigation}: LeftModalNavigatorProps) { const screenOptions = useSideModalStackScreenOptions('horizontal-inverted'); return ( - - {!shouldUseNarrowLayout && ( - - )} - - - + + {!shouldUseNarrowLayout && ( + - - - + )} + + + + + + + ); } diff --git a/src/libs/Navigation/AppNavigator/Navigators/NarrowPaneContext.tsx b/src/libs/Navigation/AppNavigator/Navigators/NarrowPaneContext.tsx new file mode 100644 index 000000000000..e08164ece8a6 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/NarrowPaneContext.tsx @@ -0,0 +1,19 @@ +import React, {createContext} from 'react'; + +type NarrowPaneContextType = { + // Whether the screen/component accessing the context is in narrow pane navigator (RHP/LHP) + isInNarrowPane: boolean; +}; + +const NarrowPaneContext = createContext({isInNarrowPane: false}); + +const IS_IN_NARROW_PANE_CONTEXT_VALUE: NarrowPaneContextType = { + isInNarrowPane: true, +}; + +function NarrowPaneContextProvider({children}: {children: React.ReactNode}) { + return {children}; +} + +export default NarrowPaneContext; +export {NarrowPaneContextProvider}; diff --git a/src/libs/Navigation/AppNavigator/Navigators/PublicRightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/PublicRightModalNavigator.tsx index dccf2e10fc75..7e17a4f74781 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/PublicRightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/PublicRightModalNavigator.tsx @@ -10,6 +10,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {ConsoleNavigatorParamList, PublicScreensParamList} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; +import {NarrowPaneContextProvider} from './NarrowPaneContext'; import Overlay from './Overlay'; type PublicRightModalNavigatorComponentProps = PlatformStackScreenProps; @@ -23,20 +24,22 @@ function PublicRightModalNavigatorComponent({navigation}: PublicRightModalNaviga const screenOptions = useCustomScreenOptions(); return ( - - {!shouldUseNarrowLayout && } - - - - - - + + + {!shouldUseNarrowLayout && } + + + + + + + ); } diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index cd90a58bfed5..366131799dfa 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -14,6 +14,7 @@ import type {AuthScreensParamList, RightModalNavigatorParamList} from '@navigati import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; +import {NarrowPaneContextProvider} from './NarrowPaneContext'; import Overlay from './Overlay'; type RightModalNavigatorProps = PlatformStackScreenProps; @@ -28,208 +29,210 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { const screenOptions = useCustomScreenOptions(); return ( - - {!shouldUseNarrowLayout && ( - { - if (isExecutingRef.current) { - return; - } - isExecutingRef.current = true; - navigation.goBack(); - setTimeout(() => { - isExecutingRef.current = false; - }, CONST.ANIMATED_TRANSITION); - }} - /> - )} - - { - if ( - // @ts-expect-error There is something wrong with a types here and it's don't see the params list - navigation.getState().routes.find((routes) => routes.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR)?.params?.screen === - SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE || - route.params?.screen !== SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE - ) { + + + {!shouldUseNarrowLayout && ( + { + if (isExecutingRef.current) { return; } - // Delay clearing review duplicate data till the RHP is completely closed - // to avoid not found showing briefly in confirmation page when RHP is closing - InteractionManager.runAfterInteractions(() => { - abandonReviewDuplicateTransactions(); - }); - }, - }} - id={NAVIGATORS.RIGHT_MODAL_NAVIGATOR} - > - { + isExecutingRef.current = false; + }, CONST.ANIMATED_TRANSITION); + }} /> - { - InteractionManager.runAfterInteractions(clearTwoFactorAuthData); + )} + + { + if ( + // @ts-expect-error There is something wrong with a types here and it's don't see the params list + navigation.getState().routes.find((routes) => routes.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR)?.params?.screen === + SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE || + route.params?.screen !== SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE + ) { + return; + } + // Delay clearing review duplicate data till the RHP is completely closed + // to avoid not found showing briefly in confirmation page when RHP is closing + InteractionManager.runAfterInteractions(() => { + abandonReviewDuplicateTransactions(); + }); }, }} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id={NAVIGATORS.RIGHT_MODAL_NAVIGATOR} + > + + { + InteractionManager.runAfterInteractions(clearTwoFactorAuthData); + }, + }} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index a3103885d9d1..1b3c92f379d6 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -44,11 +44,16 @@ import KeyboardUtils from '@src/utils/keyboard'; const excludedGroupEmails: string[] = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); +type SelectedOption = ListItem & + Omit & { + reportID?: string; + }; + function useOptions() { const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const [selectedOptions, setSelectedOptions] = useState>([]); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT); + const [selectedOptions, setSelectedOptions] = useState([]); + const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, {canBeMissing: true}); const personalData = useCurrentUserPersonalDetails(); const {didScreenTransitionEnd} = useScreenWrapperTransitionStatus(); const {options: listOptions, areOptionsInitialized} = useOptionsList({ @@ -142,7 +147,7 @@ function NewChatPage() { const styles = useThemeStyles(); const personalData = useCurrentUserPersonalDetails(); const {top} = useSafeAreaInsets(); - const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const selectionListRef = useRef(null); const {headerMessage, searchTerm, debouncedSearchTerm, setSearchTerm, selectedOptions, setSelectedOptions, recentReports, personalDetails, userToInvite, areOptionsInitialized} = @@ -152,7 +157,7 @@ function NewChatPage() { const sectionsList: Section[] = []; let firstKey = ''; - const formatResults = formatSectionsFromSearchTerm(debouncedSearchTerm, selectedOptions, recentReports, personalDetails); + const formatResults = formatSectionsFromSearchTerm(debouncedSearchTerm, selectedOptions as OptionData[], recentReports, personalDetails); sectionsList.push(formatResults.section); if (!firstKey) { @@ -198,12 +203,12 @@ function NewChatPage() { (option: ListItem & Partial) => { const isOptionInList = !!option.isSelected; - let newSelectedOptions; + let newSelectedOptions: SelectedOption[]; if (isOptionInList) { newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); } else { - newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID ?? `${CONST.DEFAULT_NUMBER_ID}`}]; + newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID}]; selectionListRef?.current?.scrollToIndex(0, true); } @@ -296,7 +301,7 @@ function NewChatPage() { if (!personalData || !personalData.login || !personalData.accountID) { return; } - const selectedParticipants: SelectedParticipant[] = selectedOptions.map((option: OptionData) => ({ + const selectedParticipants: SelectedParticipant[] = selectedOptions.map((option) => ({ login: option?.login, accountID: option.accountID ?? CONST.DEFAULT_NUMBER_ID, })); @@ -309,7 +314,7 @@ function NewChatPage() { const footerContent = useMemo( () => - !isDismissed || selectedOptions.length ? ( + (!isDismissed || selectedOptions.length > 0) && ( <> )} - ) : null, + ), [createGroup, selectedOptions.length, styles.mb5, translate, isDismissed], ); @@ -335,6 +340,8 @@ function NewChatPage() { enableEdgeToEdgeBottomSafeAreaPadding includePaddingTop={false} shouldEnablePickerAvoiding={false} + disableOfflineIndicatorSafeAreaPadding + shouldShowOfflineIndicator={false} keyboardVerticalOffset={variables.contentHeaderHeight + top + variables.tabSelectorButtonHeight + variables.tabSelectorButtonPadding} // Disable the focus trap of this page to activate the parent focus trap in `NewChatSelectorPage`. focusTrapSettings={{active: false}} diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 5e823caf5541..acad336394e8 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -17,7 +17,6 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import {addErrorMessage} from '@libs/ErrorUtils'; @@ -34,6 +33,33 @@ import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +function EmptyWorkspaceView() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true, additionalPaddingBottom: styles.mb5.marginBottom, styleProperty: 'marginBottom'}); + + return ( + <> + +