diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index e53be6f6b269..2cfd24f13ab7 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -320,14 +320,17 @@ An example of this can be seen in the [ACHContractStep](https://github.com/Expen ### Safe Area Padding -Any `FormProvider.js` that has a button will also add safe area padding by default. If the `` is inside a ``, we will want to disable the default safe area padding applied there e.g. +Any `FormProvider.tsx` that has a button at the bottom. If the `` is inside a ``, the bottom safe area inset is handled automatically (`includeSafeAreaPaddingBottom` needs to be set to `true`, but its the default). +If you have custom requirements and can't use ``, you can use the `useStyledSafeAreaInsets()` hook: ```jsx - +const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets(); + + {...} - + ``` ### Handling nested Pickers in Form diff --git a/src/components/CategorySelector/CategorySelectorModal.tsx b/src/components/CategorySelector/CategorySelectorModal.tsx index 1362de468014..f12a52dbb314 100644 --- a/src/components/CategorySelector/CategorySelectorModal.tsx +++ b/src/components/CategorySelector/CategorySelectorModal.tsx @@ -42,7 +42,7 @@ function CategorySelectorModal({policyID, isVisible, currentCategory, onCategory diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx index 6170b81073a2..2f6cb408292e 100644 --- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx +++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx @@ -57,7 +57,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear {children}; + return {children}; } FixedFooter.displayName = 'FixedFooter'; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index d26276d0418b..64bb2173f5b0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -6,10 +6,9 @@ import {Keyboard} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormElement from '@components/FormElement'; -import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollView from '@components/ScrollView'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type {OnyxFormKey} from '@src/ONYXKEYS'; @@ -60,6 +59,7 @@ function FormWrapper({ isSubmitDisabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); + const {paddingBottom: safeAreaInsetPaddingBottom} = useStyledSafeAreaInsets(); const formRef = useRef(null); const formContentRef = useRef(null); @@ -99,11 +99,12 @@ function FormWrapper({ }, [errors, formState?.errorFields, inputRefs]); const scrollViewContent = useCallback( - (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( + () => ( {children} {isSubmitButtonVisible && ( @@ -128,7 +129,8 @@ function FormWrapper({ [ formID, style, - styles.pb5, + safeAreaInsetPaddingBottom, + styles.pb5.paddingBottom, styles.mh0, styles.mt5, styles.flex1, @@ -153,33 +155,27 @@ function FormWrapper({ ); if (!shouldUseScrollView) { - return scrollViewContent({}); + return scrollViewContent(); } - return ( - - {({safeAreaPaddingBottomStyle}) => - scrollContextEnabled ? ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) : ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) - } - + return scrollContextEnabled ? ( + + {scrollViewContent()} + + ) : ( + + {scrollViewContent()} + ); } diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 205bea93f84a..fabb5e54cb60 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -2,7 +2,6 @@ import type {Ref} from 'react'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle'; import useThemeStyles from '@hooks/useThemeStyles'; import getPlatform from '@libs/getPlatform'; import CONST from '@src/CONST'; @@ -87,7 +86,6 @@ function FormAlertWithSubmitButton({ }: FormAlertWithSubmitButtonProps) { const styles = useThemeStyles(); const style = [!footerContent ? {} : styles.mb3, buttonStyles]; - const safePaddingBottomStyle = useSafePaddingBottomStyle(); // Disable pressOnEnter for Android Native to avoid issues with the Samsung keyboard, // where pressing Enter saves the form instead of adding a new line in multiline input. @@ -97,7 +95,7 @@ function FormAlertWithSubmitButton({ return ( please use `useStyledSafeAreaInsets` instead. */ function SafeAreaConsumer({children}: SafeAreaConsumerProps) { const StyleUtils = useStyleUtils(); diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index ae582867e070..e264b7fd9a55 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -10,6 +10,7 @@ import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardState from '@hooks/useKeyboardState'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -25,7 +26,6 @@ import HeaderGap from './HeaderGap'; import ImportedStateIndicator from './ImportedStateIndicator'; import KeyboardAvoidingView from './KeyboardAvoidingView'; import OfflineIndicator from './OfflineIndicator'; -import SafeAreaConsumer from './SafeAreaConsumer'; import withNavigationFallback from './withNavigationFallback'; type ScreenWrapperChildrenProps = { @@ -105,7 +105,11 @@ type ScreenWrapperProps = { focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings']; }; -type ScreenWrapperStatusContextType = {didScreenTransitionEnd: boolean}; +type ScreenWrapperStatusContextType = { + didScreenTransitionEnd: boolean; + isSafeAreaTopPaddingApplied: boolean; + isSafeAreaBottomPaddingApplied: boolean; +}; const ScreenWrapperStatusContext = createContext(undefined); @@ -233,96 +237,86 @@ function ScreenWrapper( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + const {insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets(); + const paddingStyle: StyleProp = {}; + + const isSafeAreaTopPaddingApplied = includePaddingTop; + if (includePaddingTop) { + paddingStyle.paddingTop = paddingTop; + } + + // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. + const isSafeAreaBottomPaddingApplied = includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator); + if (isSafeAreaBottomPaddingApplied) { + paddingStyle.paddingBottom = paddingBottom; + } + const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit()); - const contextValue = useMemo(() => ({didScreenTransitionEnd}), [didScreenTransitionEnd]); + const contextValue = useMemo( + () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied}), + [didScreenTransitionEnd, isSafeAreaBottomPaddingApplied, isSafeAreaTopPaddingApplied], + ); return ( - - {({ - insets = { - top: 0, - bottom: 0, - left: 0, - right: 0, - }, - paddingTop, - paddingBottom, - safeAreaPaddingBottomStyle, - }) => { - const paddingStyle: StyleProp = {}; - - if (includePaddingTop) { - paddingStyle.paddingTop = paddingTop; - } - - // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. - if (includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator)) { - paddingStyle.paddingBottom = paddingBottom; - } - - return ( - - + + + + - - - - - {isDevelopment && } - - { - // If props.children is a function, call it to provide the insets to the children. - typeof children === 'function' - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children - } - {isSmallScreenWidth && shouldShowOfflineIndicator && ( - <> - - {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} - - - )} - {!shouldUseNarrowLayout && shouldShowOfflineIndicatorInWideScreen && ( - <> - - {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} - - - )} - - - - - - - ); - }} - + + {isDevelopment && } + + { + // If props.children is a function, call it to provide the insets to the children. + typeof children === 'function' + ? children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd, + }) + : children + } + {isSmallScreenWidth && shouldShowOfflineIndicator && ( + <> + + {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} + + + )} + {!shouldUseNarrowLayout && shouldShowOfflineIndicatorInWideScreen && ( + <> + + {/* 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/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 102c632ec0e4..87d2b2c8af29 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -10,7 +10,6 @@ import Checkbox from '@components/Checkbox'; import FixedFooter from '@components/FixedFooter'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import {PressableWithFeedback} from '@components/Pressable'; -import SafeAreaConsumer from '@components/SafeAreaConsumer'; import SectionList from '@components/SectionList'; import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; @@ -22,6 +21,7 @@ import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useSingleExecution from '@hooks/useSingleExecution'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset'; import Log from '@libs/Log'; @@ -768,91 +768,89 @@ function BaseSelectionList( }, ); + const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets(); + + // TODO: test _every_ component that uses SelectionList return ( - - {({safeAreaPaddingBottomStyle}) => ( - - {shouldShowTextInput && !shouldShowTextInputAfterHeader && renderInput()} - {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} - {/* This is misleading because we might be in the process of loading fresh options from the server. */} - {(!isLoadingNewOptions || headerMessage !== translate('common.noResultsFound') || (flattenedSections.allOptions.length === 0 && !showLoadingPlaceholder)) && - !!headerMessage && ( - - {headerMessage} - - )} - {!!headerContent && headerContent} - {flattenedSections.allOptions.length === 0 && (showLoadingPlaceholder || shouldShowListEmptyContent) ? ( - renderListEmptyContent() - ) : ( - <> - {!listHeaderContent && header()} - ( - <> - {renderSectionHeader(arg)} - {listHeaderContent && header()} - - )} - renderItem={renderItem} - getItemLayout={getItemLayout} - onScroll={onScroll} - onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item, index) => item.keyForList ?? `${index}`} - extraData={focusedIndex} - // the only valid values on the new arch are "white", "black", and "default", other values will cause a crash - indicatorStyle="white" - keyboardShouldPersistTaps="always" - showsVerticalScrollIndicator={showScrollIndicator} - initialNumToRender={12} - maxToRenderPerBatch={maxToRenderPerBatch} - windowSize={windowSize} - updateCellsBatchingPeriod={updateCellsBatchingPeriod} - viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} - testID="selection-list" - onLayout={onSectionListLayout} - style={[(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0, sectionListStyle]} - ListHeaderComponent={ - shouldShowTextInput && shouldShowTextInputAfterHeader ? ( - <> - {listHeaderContent} - {renderInput()} - - ) : ( - listHeaderContent - ) - } - ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} - onEndReached={onEndReached} - onEndReachedThreshold={onEndReachedThreshold} - scrollEventThrottle={scrollEventThrottle} - contentContainerStyle={contentContainerStyle} - CellRendererComponent={shouldPreventActiveCellVirtualization ? FocusAwareCellRendererComponent : undefined} - /> - {children} - - )} - {showConfirmButton && ( - -