Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
946e3ac
fix: modals bottom safe area handling
chrispader Aug 19, 2025
d89ca04
Merge branch 'main' into @chrispader/fix-bottom-safe-area-padding-in-…
chrispader Aug 19, 2025
56e8462
move variable
chrispader Aug 19, 2025
5f2c290
style: update line spacing
chrispader Aug 19, 2025
349891c
refactor: migrate ConfirmModal
chrispader Aug 19, 2025
625898d
Merge branch 'main' into pr/57181
chrispader Oct 13, 2025
7270724
Merge branch 'main' into pr/57181
chrispader Feb 19, 2026
32031b8
fix: remove unused component
chrispader Feb 19, 2026
345e201
fix: RN modal translucency patch
chrispader Feb 20, 2026
1757096
fix: remove unsupported `android: enforceNavigationBarContrast` style…
chrispader Feb 23, 2026
a6af79d
fix: add `android:navigationBarColor` style to v29
chrispader Feb 23, 2026
f275350
fix: update modal transparent navigation bar patch
chrispader Feb 23, 2026
9651bed
fix: remove duplicate comment
chrispader Feb 23, 2026
41513b8
fix: add bottom safe are padding to `ReportActionContextMenu`
chrispader Feb 23, 2026
5b9bf2c
fix: disable modal container style in edge-to-edge mode
chrispader Feb 23, 2026
8884d74
fix: make `BaseModal` component RC compatible
chrispader Feb 23, 2026
2181dab
fix: invalid style
chrispader Feb 23, 2026
d10f5dd
refactor: `DatePickerModal`
chrispader Feb 23, 2026
e05e885
fix: omit `style` property, since we are not using it
chrispader Feb 23, 2026
7f65b86
fix: `EmojiPicker` modal
chrispader Feb 23, 2026
b63064f
Merge branch 'main' into pr/57181
chrispader Apr 13, 2026
73a9a8b
fix: prettier errors
chrispader Apr 13, 2026
96da0d7
fix: spell check
chrispader Apr 13, 2026
7a9ba4e
fix: TS errors
chrispader Apr 13, 2026
ffa83ed
revert: navigation bar translucency changes
chrispader Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/AccountSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
/>
)}
</>
Expand Down
4 changes: 3 additions & 1 deletion src/components/ConfirmContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -143,6 +144,7 @@ function ConfirmContent({
const theme = useTheme();
const {isOffline} = useNetwork();
const icons = useMemoizedLazyExpensifyIcons(['Close']);
const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true});

const isCentered = shouldCenterContent;

Expand All @@ -161,7 +163,7 @@ function ConfirmContent({
</View>
)}

<View style={[styles.m5, contentStyles]}>
<View style={[styles.m5, contentStyles, bottomSafeAreaPaddingStyle]}>
{shouldShowDismissIcon && (
<View style={styles.alignItemsEnd}>
<Tooltip text={translate('common.close')}>
Expand Down
1 change: 1 addition & 0 deletions src/components/ConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ function ConfirmModal({
restoreFocusType={restoreFocusType}
shouldHandleNavigationBack={shouldHandleNavigationBack}
shouldIgnoreBackHandlerDuringTransition={shouldIgnoreBackHandlerDuringTransition}
enableEdgeToEdgeBottomSafeAreaPadding
>
<ConfirmContent
title={title}
Expand Down
20 changes: 13 additions & 7 deletions src/components/DatePicker/DatePickerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {setYear} from 'date-fns';
import React, {useEffect, useRef, useState} from 'react';
import type {View} from 'react-native';
import {View} from 'react-native';
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {setDraftValues} from '@userActions/FormActions';
Expand Down Expand Up @@ -60,6 +61,8 @@ function DatePickerModal({
setSelectedDate(newValue);
};

const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true});

return (
<PopoverWithMeasuredContent
anchorRef={anchorRef}
Expand All @@ -76,13 +79,16 @@ function DatePickerModal({
shouldSkipRemeasurement
forwardedFSClass={forwardedFSClass}
shouldDisplayBelowModals
enableEdgeToEdgeBottomSafeAreaPadding
>
<CalendarPicker
minDate={minDate}
maxDate={maxDate}
value={selectedDate}
onSelected={handleDateSelection}
/>
<View style={bottomSafeAreaPaddingStyle}>
<CalendarPicker
minDate={minDate}
maxDate={maxDate}
value={selectedDate}
onSelected={handleDateSelection}
/>
</View>
</PopoverWithMeasuredContent>
);
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/EmojiPicker/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -235,6 +236,8 @@ function EmojiPicker({viewportOffsetTop, ref}: EmojiPickerProps) {
};
}, [isEmojiPickerVisible, shouldUseNarrowLayout, emojiPopoverAnchorOrigin, getEmojiPopoverAnchor, hideEmojiPicker]);

const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true});

return (
<PopoverWithMeasuredContent
shouldHandleNavigationBack={isMobileChrome()}
Expand Down Expand Up @@ -266,7 +269,7 @@ function EmojiPicker({viewportOffsetTop, ref}: EmojiPickerProps) {
>
<FocusTrapForModal active={isEmojiPickerVisible}>
<Activity mode={isEmojiPickerVisible ? 'visible' : 'hidden'}>
<View>
<View style={bottomSafeAreaPaddingStyle}>
<EmojiPickerMenu
onEmojiSelected={selectEmoji}
activeEmoji={activeEmoji.current}
Expand Down
109 changes: 39 additions & 70 deletions src/components/Modal/BaseModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
import type {LayoutChangeEvent} from 'react-native';
// Animated required for side panel navigation
// eslint-disable-next-line no-restricted-imports
Expand Down Expand Up @@ -63,7 +63,7 @@ function BaseModal({
swipeDirection,
shouldPreventScrollOnFocus = false,
enableEdgeToEdgeBottomSafeAreaPadding,
shouldApplySidePanelOffset = type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED,
shouldApplySidePanelOffset: shouldApplySidePanelOffsetProp,
hasBackdrop,
backdropOpacity,
shouldDisableBottomSafeAreaPadding = false,
Expand All @@ -73,8 +73,6 @@ function BaseModal({
shouldDisplayBelowModals = false,
shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode = true,
}: BaseModalProps) {
// When the `enableEdgeToEdgeBottomSafeAreaPadding` prop is explicitly set, we enable edge-to-edge mode.
const isUsingEdgeToEdgeMode = enableEdgeToEdgeBottomSafeAreaPadding !== undefined;
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand All @@ -85,6 +83,9 @@ function BaseModal({
const {isSmallScreenWidth, shouldUseNarrowLayout, isInNarrowPaneModal, isInLandscapeMode} = useResponsiveLayout();

const {sidePanelOffset} = useSidePanelState();

// This prop does not have a default value, because React Compiler throws an internal error if this is provided as a default value.
const shouldApplySidePanelOffset = shouldApplySidePanelOffsetProp ?? type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED;
const sidePanelAnimatedStyle = shouldApplySidePanelOffset && !isSmallScreenWidth ? {transform: [{translateX: Animated.multiply(sidePanelOffset.current, -1)}]} : undefined;
const keyboardStateContextValue = useKeyboardState();

Expand All @@ -98,13 +99,13 @@ function BaseModal({

const wasVisible = usePrevious(isVisible);

const uniqueModalId = useMemo(() => 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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/components/Popover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +89,7 @@ function Popover(props: PopoverProps) {
onLayout={onLayout}
animationIn={animationIn}
animationOut={animationOut}
enableEdgeToEdgeBottomSafeAreaPadding={enableEdgeToEdgeBottomSafeAreaPadding}
/>,
document.body,
);
Expand Down Expand Up @@ -120,6 +122,7 @@ function Popover(props: PopoverProps) {
onLayout={onLayout}
animationIn={animationIn}
animationOut={animationOut}
enableEdgeToEdgeBottomSafeAreaPadding={enableEdgeToEdgeBottomSafeAreaPadding}
/>
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/components/Popover/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading
Loading