diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index 2cfd24f13ab7..aea19e24470c 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -340,3 +340,7 @@ In case there's a nested Picker in Form, we should pass the props below to Form, #### Enable ScrollContext Pass the `scrollContextEnabled` prop to enable scrolling up when Picker is pressed, making sure the Picker is always in view and doesn't get covered by virtual keyboards for example. + +#### Enable Form to Scroll to the End + +Pass the `shouldScrollToEnd` prop to automatically scroll to the bottom when the form is opened. Ensure that the scrolling stops at the appropriate limit so that the button remains visible above the keypad. diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 43fc9cd7af78..cdc9a7129fa0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useMemo, useRef} from 'react'; import type {RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native'; -import {Keyboard} from 'react-native'; +import {InteractionManager, Keyboard} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormElement from '@components/FormElement'; @@ -61,6 +61,7 @@ function FormWrapper({ disablePressOnEnter = false, isSubmitDisabled = false, isLoading = false, + shouldScrollToEnd = false, }: FormWrapperProps) { const styles = useThemeStyles(); const {paddingBottom: safeAreaInsetPaddingBottom} = useStyledSafeAreaInsets(); @@ -109,6 +110,16 @@ function FormWrapper({ ref={formContentRef} // Note: the paddingBottom is only grater 0 if no parent has applied the inset yet: style={[style, {paddingBottom: safeAreaInsetPaddingBottom + styles.pb5.paddingBottom}]} + onLayout={() => { + if (!shouldScrollToEnd) { + return; + } + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + formRef.current?.scrollToEnd({animated: true}); + }); + }); + }} > {children} {isSubmitButtonVisible && ( @@ -156,6 +167,7 @@ function FormWrapper({ enabledWhenOffline, isSubmitActionDangerous, disablePressOnEnter, + shouldScrollToEnd, ], ); diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 02cc4e899b32..72a00ec98e5a 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -163,6 +163,12 @@ type FormProps = { /** Disable press on enter for submit button */ disablePressOnEnter?: boolean; + + /** + * Determines whether the form should automatically scroll to the end upon rendering or when the value changes. + * If `true`, the form will smoothly scroll to the bottom after interactions have completed. + */ + shouldScrollToEnd?: boolean; }; type FormRef = { diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index e99307897315..c340d6e03930 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -22,7 +22,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as User from '@userActions/User'; +import {clearCustomStatus, clearDraftCustomStatus, updateCustomStatus, updateDraftCustomStatus} from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -100,12 +100,12 @@ function StatusPage() { setBrickRoadIndicator(isValidClearAfterDate() ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); return; } - User.updateCustomStatus({ + updateCustomStatus({ text: statusText, emojiCode: !emojiCode && statusText ? initialEmoji : emojiCode, clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '', }); - User.clearDraftCustomStatus(); + clearDraftCustomStatus(); navigateBackToPreviousScreenTask.current = InteractionManager.runAfterInteractions(() => { navigateBackToPreviousScreen(); }); @@ -117,8 +117,8 @@ function StatusPage() { if (navigateBackToPreviousScreenTask.current) { return; } - User.clearCustomStatus(); - User.updateDraftCustomStatus({ + clearCustomStatus(); + updateDraftCustomStatus({ text: '', emojiCode: '', clearAfter: DateUtils.getEndOfToday(), @@ -134,12 +134,12 @@ function StatusPage() { useEffect(() => { if (!currentUserEmojiCode && !currentUserClearAfter && !draftClearAfter) { - User.updateDraftCustomStatus({clearAfter: DateUtils.getEndOfToday()}); + updateDraftCustomStatus({clearAfter: DateUtils.getEndOfToday()}); } else { - User.updateDraftCustomStatus({clearAfter: currentUserClearAfter}); + updateDraftCustomStatus({clearAfter: currentUserClearAfter}); } - return () => User.clearDraftCustomStatus(); + return () => clearDraftCustomStatus(); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -158,6 +158,7 @@ function StatusPage() { shouldEnablePickerAvoiding={false} includeSafeAreaPaddingBottom testID={HeaderPageLayout.displayName} + shouldEnableMaxHeight > {translate('statusPage.statusExplanation')}