diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 0247f8cd19c1..f6c7ba6d3441 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -306,12 +306,12 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) { }} menuItems={menuItems()} headerText={translate('delegate.switchAccount')} - containerStyles={[{maxHeight: windowHeight / 2}, styles.pb0, styles.mw100, shouldUseNarrowLayout ? {} : styles.wFitContent]} + containerStyles={[{maxHeight: windowHeight / 2}, styles.mw100, shouldUseNarrowLayout ? {} : styles.wFitContent]} headerStyles={styles.pt0} innerContainerStyle={styles.pb0} - scrollContainerStyle={styles.pb4} shouldUseScrollView shouldUpdateFocusedIndex={false} + enableEdgeToEdgeBottomSafeAreaPadding /> )} diff --git a/src/components/ConfirmContent.tsx b/src/components/ConfirmContent.tsx index e00a6024fe1a..5d5d7b0a2ded 100644 --- a/src/components/ConfirmContent.tsx +++ b/src/components/ConfirmContent.tsx @@ -2,6 +2,7 @@ import type {ReactNode} from 'react'; import React from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -143,6 +144,7 @@ function ConfirmContent({ const theme = useTheme(); const {isOffline} = useNetwork(); const icons = useMemoizedLazyExpensifyIcons(['Close']); + const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true}); const isCentered = shouldCenterContent; @@ -161,7 +163,7 @@ function ConfirmContent({ )} - + {shouldShowDismissIcon && ( diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index d85baa8cd0fc..ea7ff9313d66 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -187,6 +187,7 @@ function ConfirmModal({ restoreFocusType={restoreFocusType} shouldHandleNavigationBack={shouldHandleNavigationBack} shouldIgnoreBackHandlerDuringTransition={shouldIgnoreBackHandlerDuringTransition} + enableEdgeToEdgeBottomSafeAreaPadding > - + + + ); } diff --git a/src/components/EmojiPicker/EmojiPicker.tsx b/src/components/EmojiPicker/EmojiPicker.tsx index 642bbba7c635..2f6bd51fa1e9 100644 --- a/src/components/EmojiPicker/EmojiPicker.tsx +++ b/src/components/EmojiPicker/EmojiPicker.tsx @@ -7,6 +7,7 @@ import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import withViewportOffsetTop from '@components/withViewportOffsetTop'; +import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -235,6 +236,8 @@ function EmojiPicker({viewportOffsetTop, ref}: EmojiPickerProps) { }; }, [isEmojiPickerVisible, shouldUseNarrowLayout, emojiPopoverAnchorOrigin, getEmojiPopoverAnchor, hideEmojiPicker]); + const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true}); + return ( - + modalId ?? ComposerFocusManager.getId(), [modalId]); - const saveFocusState = useCallback(() => { + const uniqueModalId = modalId ?? ComposerFocusManager.getId(); + const saveFocusState = () => { if (shouldEnableNewFocusManagement) { ComposerFocusManager.saveFocusState(uniqueModalId); } ComposerFocusManager.resetReadyToFocus(uniqueModalId); - }, [shouldEnableNewFocusManagement, uniqueModalId]); + }; /** * Hides modal * @param callHideCallback - Should we call the onModalHide callback @@ -166,18 +167,17 @@ function BaseModal({ } hideModalCallbackRef.current?.(true); }, - // eslint-disable-next-line react-hooks/exhaustive-deps [], ); useEffect(() => () => DeviceEventEmitter.emit(CONST.MODAL_EVENTS.CLOSED), []); - const handleShowModal = useCallback(() => { + const handleShowModal = () => { if (shouldSetModalVisibility) { setModalVisibility(true, type); } onModalShow(); - }, [onModalShow, shouldSetModalVisibility, type]); + }; const handleBackdropPress = (e?: KeyboardEvent) => { if (e?.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) { @@ -227,81 +227,50 @@ function BaseModal({ shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding, hideBackdrop, - } = useMemo( - () => - StyleUtils.getModalStyles( - type, - { - windowWidth, - windowHeight, - isSmallScreenWidth, - shouldUseNarrowLayout, - }, - popoverAnchorPosition, - innerContainerStyle, - outerStyle, - shouldUseModalPaddingStyle, - { - modalOverlapsWithTopSafeArea, - shouldDisableBottomSafeAreaPadding: !!shouldDisableBottomSafeAreaPadding, - }, - shouldDisplayBelowModals, - ), - [ - StyleUtils, - type, + } = StyleUtils.getModalStyles({ + type, + windowDimensions: { windowWidth, windowHeight, isSmallScreenWidth, shouldUseNarrowLayout, - popoverAnchorPosition, - innerContainerStyle, - outerStyle, - shouldUseModalPaddingStyle, + }, + popoverAnchorPosition, + innerContainerStyle, + outerStyle, + shouldUseModalPaddingStyle, + safeAreaOptions: { modalOverlapsWithTopSafeArea, - shouldDisableBottomSafeAreaPadding, - shouldDisplayBelowModals, - ], - ); + shouldDisableBottomSafeAreaPadding: !!shouldDisableBottomSafeAreaPadding, + }, + enableEdgeToEdgeBottomSafeAreaPadding, + shouldDisplayBelowModals, + }); - const modalPaddingStyles = useMemo(() => { - const paddings = StyleUtils.getModalPaddingStyles({ - shouldAddBottomSafeAreaMargin, - shouldAddTopSafeAreaMargin, - // enableEdgeToEdgeBottomSafeAreaPadding is used as a temporary solution to disable safe area bottom spacing on modals, to allow edge-to-edge content - shouldAddBottomSafeAreaPadding: !isUsingEdgeToEdgeMode && (!avoidKeyboard || !keyboardStateContextValue.isKeyboardActive) && shouldAddBottomSafeAreaPadding, - shouldAddTopSafeAreaPadding, - modalContainerStyle, - insets, - }); - return shouldUseModalPaddingStyle ? paddings : {paddingLeft: paddings.paddingLeft, paddingRight: paddings.paddingRight}; - }, [ - StyleUtils, - avoidKeyboard, - insets, - isUsingEdgeToEdgeMode, - keyboardStateContextValue.isKeyboardActive, - modalContainerStyle, + // When the `enableEdgeToEdgeBottomSafeAreaPadding` prop is explicitly set, we enable edge-to-edge mode. + const isUsingEdgeToEdgeMode = enableEdgeToEdgeBottomSafeAreaPadding !== undefined; + + const paddings = StyleUtils.getModalPaddingStyles({ shouldAddBottomSafeAreaMargin, - shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaMargin, + // enableEdgeToEdgeBottomSafeAreaPadding is used as a temporary solution to disable safe area bottom spacing on modals, to allow edge-to-edge content + shouldAddBottomSafeAreaPadding: !isUsingEdgeToEdgeMode && (!avoidKeyboard || !keyboardStateContextValue.isKeyboardActive) && shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaPadding, - shouldUseModalPaddingStyle, - ]); + modalContainerStyle, + insets, + }); + const modalPaddingStyles = shouldUseModalPaddingStyle ? paddings : {paddingLeft: paddings.paddingLeft, paddingRight: paddings.paddingRight}; - const modalContextValue = useMemo( - () => ({ - activeModalType: isVisible ? type : undefined, - default: false, - }), - [isVisible, type], - ); + const modalContextValue = { + activeModalType: isVisible ? type : undefined, + default: false, + }; // 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]); + const offlineIndicatorContextValue = isInNarrowPane ? (originalValues ?? {}) : {}; const backdropOpacityAdjusted = hideBackdrop || (type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED && !isSmallScreenWidth && (isInNarrowPane || isInNarrowPaneModal)) // right_docked modals shouldn't add backdrops when opened in same-width RHP diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index 6584b9b7078f..da7c543647aa 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -31,6 +31,7 @@ function Popover(props: PopoverProps) { animationIn = 'fadeIn', animationOut = 'fadeOut', shouldCloseWhenBrowserNavigationChanged = true, + enableEdgeToEdgeBottomSafeAreaPadding = false, } = props; // We need to use isSmallScreenWidth to apply the correct modal type and popoverAnchorPosition @@ -88,6 +89,7 @@ function Popover(props: PopoverProps) { onLayout={onLayout} animationIn={animationIn} animationOut={animationOut} + enableEdgeToEdgeBottomSafeAreaPadding={enableEdgeToEdgeBottomSafeAreaPadding} />, document.body, ); @@ -120,6 +122,7 @@ function Popover(props: PopoverProps) { onLayout={onLayout} animationIn={animationIn} animationOut={animationOut} + enableEdgeToEdgeBottomSafeAreaPadding={enableEdgeToEdgeBottomSafeAreaPadding} /> ); } diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index f7917bee6d7f..71cfef172774 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -38,6 +38,13 @@ type PopoverProps = BaseModalProps & /** Whether we should display the popover below other modals (e.g. SidePanel, RHP) */ shouldDisplayBelowModals?: boolean; + + /** + * Temporary flag to disable safe area bottom spacing in modals and to allow edge-to-edge content. + * Modals should not always apply bottom safe area padding, instead it should be applied to the scrollable/bottom-docked content directly. + * This flag can be removed, once all components/screens have switched to edge-to-edge safe area handling. + */ + enableEdgeToEdgeBottomSafeAreaPadding?: boolean; }; export default PopoverProps; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 39eaa309984e..b4b7d938fb12 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -5,6 +5,7 @@ import React, {useCallback, useLayoutEffect, useMemo, useState} from 'react'; import {StyleSheet, View} from 'react-native'; import type {GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -185,15 +186,38 @@ type PopoverMenuProps = Partial & { /** Badge style to be shown near the right end. */ badgeStyle?: StyleProp; + + /** + * Temporary flag to disable safe area bottom spacing in modals and to allow edge-to-edge content. + * Modals should not always apply bottom safe area padding, instead it should be applied to the scrollable/bottom-docked content directly. + * This flag can be removed, once all components/screens have switched to edge-to-edge safe area handling. + */ + enableEdgeToEdgeBottomSafeAreaPadding?: boolean; }; -const renderWithConditionalWrapper = (shouldUseScrollView: boolean, contentContainerStyle: StyleProp, children: ReactNode): React.JSX.Element => { +type PopoverMenuContentProps = { + shouldUseScrollView: boolean; + contentContainerStyle: StyleProp; + children: ReactNode; + addBottomSafeAreaPadding?: boolean; +}; + +function PopoverMenuContent({shouldUseScrollView, contentContainerStyle, children, addBottomSafeAreaPadding}: PopoverMenuContentProps): React.JSX.Element { + const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding}); + if (shouldUseScrollView) { - return {children}; + return ( + + {children} + + ); } // eslint-disable-next-line react/jsx-no-useless-fragment - return {children}; -}; + return {children}; +} function getSelectedItemIndex(menuItems: PopoverMenuItem[]) { return menuItems.findIndex((option) => option.isSelected); @@ -303,6 +327,7 @@ function BasePopoverMenu({ shouldUseModalPaddingStyle, shouldAvoidSafariException = false, shouldMaintainFocusAfterSubItemSelect: shouldPreserveFocusOnSubItems = true, + enableEdgeToEdgeBottomSafeAreaPadding, testID, }: PopoverMenuProps) { const styles = useThemeStyles(); @@ -554,7 +579,7 @@ function BasePopoverMenu({ } return stylesArray; - }, [isSmallScreenWidth, shouldEnableMaxHeight, styles.createMenuContainer, shouldUseScrollView]); + }, [isSmallScreenWidth, styles.createMenuContainer, shouldUseScrollView, shouldEnableMaxHeight, isInLandscapeMode]); const {paddingTop, paddingBottom, paddingVertical, ...restScrollContainerStyle} = (StyleSheet.flatten([styles.pv4, scrollContainerStyle]) as ViewStyle) ?? {}; const { @@ -619,6 +644,7 @@ function BasePopoverMenu({ shouldHandleNavigationBack={shouldHandleNavigationBack} testID={testID} shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode={!shouldUseScrollView} + enableEdgeToEdgeBottomSafeAreaPadding={enableEdgeToEdgeBottomSafeAreaPadding} > - {renderWithConditionalWrapper( - shouldUseScrollView, - [scrollViewPaddingStyles, restScrollContainerStyle], - [renderHeaderText(), enteredSubMenuIndexes.length > 0 && renderBackButtonItem(), renderedMenuItems], - )} + + {renderHeaderText()} + {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} + {renderedMenuItems} + ); } -PopoverMenu.displayName = 'PopoverMenu'; - export default React.memo( PopoverMenu, (prevProps, nextProps) => diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index 05f1058fc29a..e78fe5b8904a 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -25,6 +25,7 @@ function PopoverWithoutOverlay({ onModalHide = () => {}, children, shouldDisplayBelowModals = false, + enableEdgeToEdgeBottomSafeAreaPadding, }: PopoverWithoutOverlayProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -32,18 +33,19 @@ function PopoverWithoutOverlay({ const {windowWidth, windowHeight} = useWindowDimensions(); const insets = useSafeAreaInsets(); const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = - StyleUtils.getModalStyles( - CONST.MODAL.MODAL_TYPE.POPOVER, - { + StyleUtils.getModalStyles({ + type: CONST.MODAL.MODAL_TYPE.POPOVER, + windowDimensions: { windowWidth, windowHeight, isSmallScreenWidth: false, }, - anchorPosition, + popoverAnchorPosition: anchorPosition, innerContainerStyle, outerStyle, shouldDisplayBelowModals, - ); + enableEdgeToEdgeBottomSafeAreaPadding, + }); useEffect(() => { let removeOnClose: () => void; @@ -78,12 +80,21 @@ function PopoverWithoutOverlay({ StyleUtils.getModalPaddingStyles({ shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaPadding, + shouldAddBottomSafeAreaPadding: enableEdgeToEdgeBottomSafeAreaPadding === undefined && shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaPadding, modalContainerStyle, insets, }), - [StyleUtils, insets, modalContainerStyle, shouldAddBottomSafeAreaMargin, shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaMargin, shouldAddTopSafeAreaPadding], + [ + StyleUtils, + enableEdgeToEdgeBottomSafeAreaPadding, + insets, + modalContainerStyle, + shouldAddBottomSafeAreaMargin, + shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaMargin, + shouldAddTopSafeAreaPadding, + ], ); if (!isVisible) { diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index 1a519ef51ed2..299de1bdc9c2 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -195,6 +195,7 @@ function ThreeDotsMenu({ anchorRef={buttonRef} shouldEnableNewFocusManagement restoreFocusType={restoreFocusType} + enableEdgeToEdgeBottomSafeAreaPadding /> ); diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 969ff12cdb6b..49eb75549b24 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -12,6 +12,7 @@ import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/ import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import {useSession} from '@components/OnyxListItemProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; @@ -368,12 +369,14 @@ function BaseReportActionContextMenu({ // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); + const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true}); + return ( (isVisible || shouldKeepOpen || !isMini) && ( {filteredContextMenuActions.map((contextAction, index) => { const closePopup = !isMini; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index bab9ff38e286..97f1eb331eab 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -475,6 +475,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro anchorDimensions={contextMenuDimensions.current} anchorRef={anchorRef} shouldSwitchPositionIfOverflow={shouldSwitchPositionIfOverflow} + enableEdgeToEdgeBottomSafeAreaPadding > GetModalStyles; + getModalStyles: (options: GetModalStylesOptions) => GetModalStyles; }; const createModalStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ - getModalStyles: ( + getModalStyles: ({ type, windowDimensions, popoverAnchorPosition = {}, @@ -61,8 +64,9 @@ const createModalStyleUtils: StyleUtilGenerator = ({the outerStyle = {}, shouldUseModalPaddingStyle = true, safeAreaOptions = {modalOverlapsWithTopSafeArea: false, shouldDisableBottomSafeAreaPadding: false}, + enableEdgeToEdgeBottomSafeAreaPadding = false, shouldDisplayBelowModals = false, - ): GetModalStyles => { + }): GetModalStyles => { const {windowWidth, isSmallScreenWidth} = windowDimensions; let modalStyle: GetModalStyles['modalStyle'] = { @@ -241,10 +245,13 @@ const createModalStyleUtils: StyleUtilGenerator = ({the if (shouldUseModalPaddingStyle) { modalContainerStyle.paddingTop = variables.componentBorderRadiusLarge; - modalContainerStyle.paddingBottom = variables.componentBorderRadiusLarge; + + if (!enableEdgeToEdgeBottomSafeAreaPadding) { + modalContainerStyle.paddingBottom = variables.componentBorderRadiusLarge; + } } - shouldAddBottomSafeAreaPadding = !safeAreaOptions?.shouldDisableBottomSafeAreaPadding; + shouldAddBottomSafeAreaPadding = !enableEdgeToEdgeBottomSafeAreaPadding && !safeAreaOptions?.shouldDisableBottomSafeAreaPadding; shouldAddTopSafeAreaMargin = !!safeAreaOptions?.modalOverlapsWithTopSafeArea; swipeDirection = undefined; animationIn = 'slideInUp'; @@ -293,7 +300,7 @@ const createModalStyleUtils: StyleUtilGenerator = ({the animationOut = 'slideOutRight'; swipeDirection = undefined; - shouldAddBottomSafeAreaPadding = true; + shouldAddBottomSafeAreaPadding = !enableEdgeToEdgeBottomSafeAreaPadding; shouldAddTopSafeAreaPadding = true; break; default: