Skip to content

Commit

Permalink
Merge pull request Expensify#27702 from chiragxarora/fix/21654
Browse files Browse the repository at this point in the history
  • Loading branch information
thienlnam authored Sep 22, 2023
2 parents 7f6e4f4 + 6675aa2 commit 28a434c
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 161 deletions.
23 changes: 23 additions & 0 deletions src/libs/UpdateMultilineInputRange/index.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Place the cursor at the end of the value (if there is a value in the input).
*
* When a multiline input contains a text value that goes beyond the scroll height, the cursor will be placed
* at the end of the text value, and automatically scroll the input field to this position after the field gains
* focus. This provides a better user experience in cases where the text in the field has to be edited. The auto-
* scroll behaviour works on all platforms except iOS native.
* See https://github.com/Expensify/App/issues/20836 for more details.
*
* @param {Object} input the input element
*/
export default function updateMultilineInputRange(input) {
if (!input) {
return;
}

/*
* Adding this iOS specific patch because of the scroll issue in native iOS
* Issue: does not scroll multiline input when text exceeds the maximum number of lines
* For more details: https://github.com/Expensify/App/pull/27702#issuecomment-1728651132
*/
input.focus();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Focus a multiline text input and place the cursor at the end of the value (if there is a value in the input).
* Place the cursor at the end of the value (if there is a value in the input).
*
* When a multiline input contains a text value that goes beyond the scroll height, the cursor will be placed
* at the end of the text value, and automatically scroll the input field to this position after the field gains
Expand All @@ -9,12 +9,11 @@
*
* @param {Object} input the input element
*/
export default function focusAndUpdateMultilineInputRange(input) {
export default function updateMultilineInputRange(input) {
if (!input) {
return;
}

input.focus();
if (input.value && input.setSelectionRange) {
const length = input.value.length;
input.setSelectionRange(length, length);
Expand Down
31 changes: 28 additions & 3 deletions src/pages/EditRequestDescriptionPage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useRef} from 'react';
import React, {useRef, useCallback} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {useFocusEffect} from '@react-navigation/native';
import TextInput from '../components/TextInput';
import ScreenWrapper from '../components/ScreenWrapper';
import HeaderWithBackButton from '../components/HeaderWithBackButton';
Expand All @@ -10,6 +11,7 @@ import styles from '../styles/styles';
import CONST from '../CONST';
import useLocalize from '../hooks/useLocalize';
import * as Browser from '../libs/Browser';
import updateMultilineInputRange from '../libs/UpdateMultilineInputRange';

const propTypes = {
/** Transaction default description value */
Expand All @@ -22,11 +24,28 @@ const propTypes = {
function EditRequestDescriptionPage({defaultDescription, onSubmit}) {
const {translate} = useLocalize();
const descriptionInputRef = useRef(null);
const focusTimeoutRef = useRef(null);

useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => {
if (descriptionInputRef.current) {
descriptionInputRef.current.focus();
}
return () => {
if (!focusTimeoutRef.current) {
return;
}
clearTimeout(focusTimeoutRef.current);
};
}, CONST.ANIMATED_TRANSITION);
}, []),
);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
onEntryTransitionEnd={() => descriptionInputRef.current && descriptionInputRef.current.focus()}
testID={EditRequestDescriptionPage.displayName}
>
<HeaderWithBackButton title={translate('common.description')} />
Expand All @@ -46,7 +65,13 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) {
label={translate('moneyRequestConfirmationList.whatsItFor')}
accessibilityLabel={translate('moneyRequestConfirmationList.whatsItFor')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
ref={(e) => (descriptionInputRef.current = e)}
ref={(el) => {
if (!el) {
return;
}
descriptionInputRef.current = el;
updateMultilineInputRange(descriptionInputRef.current);
}}
autoGrowHeight
containerStyles={[styles.autoGrowHeightMultilineInput]}
textAlignVertical="top"
Expand Down
31 changes: 27 additions & 4 deletions src/pages/PrivateNotes/PrivateNotesEditPage.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, {useState, useRef} from 'react';
import React, {useState, useRef, useCallback} from 'react';
import PropTypes from 'prop-types';
import {View, Keyboard} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import {useFocusEffect} from '@react-navigation/native';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
Expand All @@ -23,7 +24,7 @@ import personalDetailsPropType from '../personalDetailsPropType';
import * as Report from '../../libs/actions/Report';
import useLocalize from '../../hooks/useLocalize';
import OfflineWithFeedback from '../../components/OfflineWithFeedback';
import focusAndUpdateMultilineInputRange from '../../libs/focusAndUpdateMultilineInputRange';
import updateMultilineInputRange from '../../libs/UpdateMultilineInputRange';
import ROUTES from '../../ROUTES';

const propTypes = {
Expand Down Expand Up @@ -66,6 +67,23 @@ function PrivateNotesEditPage({route, personalDetailsList, session, report}) {

// To focus on the input field when the page loads
const privateNotesInput = useRef(null);
const focusTimeoutRef = useRef(null);

useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => {
if (privateNotesInput.current) {
privateNotesInput.current.focus();
}
return () => {
if (!focusTimeoutRef.current) {
return;
}
clearTimeout(focusTimeoutRef.current);
};
}, CONST.ANIMATED_TRANSITION);
}, []),
);

const savePrivateNote = () => {
const editedNote = parser.replace(privateNote);
Expand All @@ -79,7 +97,6 @@ function PrivateNotesEditPage({route, personalDetailsList, session, report}) {
return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
onEntryTransitionEnd={() => focusAndUpdateMultilineInputRange(privateNotesInput.current)}
testID={PrivateNotesEditPage.displayName}
>
<FullPageNotFoundView
Expand Down Expand Up @@ -128,7 +145,13 @@ function PrivateNotesEditPage({route, personalDetailsList, session, report}) {
defaultValue={privateNote}
value={privateNote}
onChangeText={(text) => setPrivateNote(text)}
ref={(el) => (privateNotesInput.current = el)}
ref={(el) => {
if (!el) {
return;
}
privateNotesInput.current = el;
updateMultilineInputRange(privateNotesInput.current);
}}
/>
</OfflineWithFeedback>
</Form>
Expand Down
104 changes: 54 additions & 50 deletions src/pages/ReportWelcomeMessagePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {View} from 'react-native';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import {useFocusEffect} from '@react-navigation/native';
import compose from '../libs/compose';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import ScreenWrapper from '../components/ScreenWrapper';
Expand All @@ -19,7 +20,7 @@ import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundVi
import Form from '../components/Form';
import * as PolicyUtils from '../libs/PolicyUtils';
import {policyPropTypes, policyDefaultProps} from './workspace/withPolicy';
import focusAndUpdateMultilineInputRange from '../libs/focusAndUpdateMultilineInputRange';
import updateMultilineInputRange from '../libs/UpdateMultilineInputRange';

const propTypes = {
...withLocalizePropTypes,
Expand All @@ -45,6 +46,7 @@ function ReportWelcomeMessagePage(props) {
const parser = new ExpensiMark();
const [welcomeMessage, setWelcomeMessage] = useState(parser.htmlToMarkdown(props.report.welcomeMessage));
const welcomeMessageInputRef = useRef(null);
const focusTimeoutRef = useRef(null);

const handleWelcomeMessageChange = useCallback((value) => {
setWelcomeMessage(value);
Expand All @@ -54,56 +56,58 @@ function ReportWelcomeMessagePage(props) {
Report.updateWelcomeMessage(props.report.reportID, props.report.welcomeMessage, welcomeMessage.trim());
}, [props.report.reportID, props.report.welcomeMessage, welcomeMessage]);

return (
<ScreenWrapper
onEntryTransitionEnd={() => {
if (!welcomeMessageInputRef.current) {
return;
useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => {
if (welcomeMessageInputRef.current) {
welcomeMessageInputRef.current.focus();
}
focusAndUpdateMultilineInputRange(welcomeMessageInputRef.current);
}}
testID={ReportWelcomeMessagePage.displayName}
>
{({didScreenTransitionEnd}) => (
<FullPageNotFoundView shouldShow={!PolicyUtils.isPolicyAdmin(props.policy)}>
<HeaderWithBackButton title={props.translate('welcomeMessagePage.welcomeMessage')} />
<Form
style={[styles.flexGrow1, styles.ph5]}
formID={ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM}
onSubmit={submitForm}
submitButtonText={props.translate('common.save')}
enabledWhenOffline
>
<Text style={[styles.mb5]}>{props.translate('welcomeMessagePage.explainerText')}</Text>
<View style={[styles.mb6]}>
<TextInput
inputID="welcomeMessage"
label={props.translate('welcomeMessagePage.welcomeMessage')}
accessibilityLabel={props.translate('welcomeMessagePage.welcomeMessage')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
autoGrowHeight
maxLength={CONST.MAX_COMMENT_LENGTH}
ref={(el) => {
// Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes
// to avoid focus multiple time, we should early return if el is null.
if (!el) {
return;
}
if (!welcomeMessageInputRef.current && didScreenTransitionEnd) {
focusAndUpdateMultilineInputRange(el);
}
welcomeMessageInputRef.current = el;
}}
value={welcomeMessage}
onChangeText={handleWelcomeMessageChange}
autoCapitalize="none"
textAlignVertical="top"
containerStyles={[styles.autoGrowHeightMultilineInput]}
/>
</View>
</Form>
</FullPageNotFoundView>
)}
return () => {
if (!focusTimeoutRef.current) {
return;
}
clearTimeout(focusTimeoutRef.current);
};
}, CONST.ANIMATED_TRANSITION);
}, []),
);

return (
<ScreenWrapper testID={ReportWelcomeMessagePage.displayName}>
<FullPageNotFoundView shouldShow={!PolicyUtils.isPolicyAdmin(props.policy)}>
<HeaderWithBackButton title={props.translate('welcomeMessagePage.welcomeMessage')} />
<Form
style={[styles.flexGrow1, styles.ph5]}
formID={ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM}
onSubmit={submitForm}
submitButtonText={props.translate('common.save')}
enabledWhenOffline
>
<Text style={[styles.mb5]}>{props.translate('welcomeMessagePage.explainerText')}</Text>
<View style={[styles.mb6]}>
<TextInput
inputID="welcomeMessage"
label={props.translate('welcomeMessagePage.welcomeMessage')}
accessibilityLabel={props.translate('welcomeMessagePage.welcomeMessage')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
autoGrowHeight
maxLength={CONST.MAX_COMMENT_LENGTH}
ref={(el) => {
if (!el) {
return;
}
welcomeMessageInputRef.current = el;
updateMultilineInputRange(welcomeMessageInputRef.current);
}}
value={welcomeMessage}
onChangeText={handleWelcomeMessageChange}
autoCapitalize="none"
textAlignVertical="top"
containerStyles={[styles.autoGrowHeightMultilineInput]}
/>
</View>
</Form>
</FullPageNotFoundView>
</ScreenWrapper>
);
}
Expand Down
Loading

0 comments on commit 28a434c

Please sign in to comment.