Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 0 deletions contributingGuides/FORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 13 additions & 1 deletion src/components/Form/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -61,6 +61,7 @@ function FormWrapper({
disablePressOnEnter = false,
isSubmitDisabled = false,
isLoading = false,
shouldScrollToEnd = false,
}: FormWrapperProps) {
const styles = useThemeStyles();
const {paddingBottom: safeAreaInsetPaddingBottom} = useStyledSafeAreaInsets();
Expand Down Expand Up @@ -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 && (
Expand Down Expand Up @@ -156,6 +167,7 @@ function FormWrapper({
enabledWhenOffline,
isSubmitActionDangerous,
disablePressOnEnter,
shouldScrollToEnd,
],
);

Expand Down
6 changes: 6 additions & 0 deletions src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {

/** 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<TFormID extends OnyxFormKey = OnyxFormKey> = {
Expand Down
18 changes: 10 additions & 8 deletions src/pages/settings/Profile/CustomStatus/StatusPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
});
Expand All @@ -117,8 +117,8 @@ function StatusPage() {
if (navigateBackToPreviousScreenTask.current) {
return;
}
User.clearCustomStatus();
User.updateDraftCustomStatus({
clearCustomStatus();
updateDraftCustomStatus({
text: '',
emojiCode: '',
clearAfter: DateUtils.getEndOfToday(),
Expand All @@ -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
}, []);

Expand All @@ -158,6 +158,7 @@ function StatusPage() {
shouldEnablePickerAvoiding={false}
includeSafeAreaPaddingBottom
testID={HeaderPageLayout.displayName}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('statusPage.status')}
Expand All @@ -172,6 +173,7 @@ function StatusPage() {
onSubmit={updateStatus}
validate={validateForm}
enabledWhenOffline
shouldScrollToEnd
>
<View style={[styles.mh5, styles.mv1]}>
<Text style={[styles.textNormal, styles.mt2]}>{translate('statusPage.statusExplanation')}</Text>
Expand Down