Skip to content

Commit

Permalink
Merge pull request #51172 from christianwen/fix/10731-auto-focus-input
Browse files Browse the repository at this point in the history
  • Loading branch information
iwiznia authored Dec 24, 2024
2 parents 52b4eca + 1954471 commit 8819d9c
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 53 deletions.
17 changes: 16 additions & 1 deletion src/components/Composer/implementation/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import mimeDb from 'mime-db';
import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native';
import {StyleSheet} from 'react-native';
import type {FileObject} from '@components/AttachmentModal';
import type {ComposerProps} from '@components/Composer/types';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useKeyboardState from '@hooks/useKeyboardState';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useStyleUtils from '@hooks/useStyleUtils';
Expand Down Expand Up @@ -37,6 +38,7 @@ function Composer(
selection,
value,
isGroupPolicyReport = false,
showSoftInputOnFocus = true,
...props
}: ComposerProps,
ref: ForwardedRef<TextInput>,
Expand All @@ -49,7 +51,11 @@ function Composer(
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();

const [contextMenuHidden, setContextMenuHidden] = useState(true);

const {inputCallbackRef, inputRef: autoFocusInputRef} = useAutoFocusInput();
const keyboardState = useKeyboardState();
const isKeyboardShown = keyboardState?.isKeyboardShown ?? false;

useEffect(() => {
if (autoFocus === !!autoFocusInputRef.current) {
Expand All @@ -58,6 +64,13 @@ function Composer(
inputCallbackRef(autoFocus ? textInput.current : null);
}, [autoFocus, inputCallbackRef, autoFocusInputRef]);

useEffect(() => {
if (!showSoftInputOnFocus || !isKeyboardShown) {
return;
}
setContextMenuHidden(false);
}, [showSoftInputOnFocus, isKeyboardShown]);

useEffect(() => {
if (!textInput.current || !textInput.current.setSelection || !selection || isComposerFullSize) {
return;
Expand Down Expand Up @@ -158,6 +171,8 @@ function Composer(
props?.onBlur?.(e);
}}
onClear={onClear}
showSoftInputOnFocus={showSoftInputOnFocus}
contextMenuHidden={contextMenuHidden}
/>
);
}
Expand Down
20 changes: 18 additions & 2 deletions src/components/Composer/implementation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {BaseSyntheticEvent, ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {NativeSyntheticEvent, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native';
import {DeviceEventEmitter, StyleSheet} from 'react-native';
import {DeviceEventEmitter, InteractionManager, StyleSheet} from 'react-native';
import type {ComposerProps} from '@components/Composer/types';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
Expand Down Expand Up @@ -50,6 +50,7 @@ function Composer(
isComposerFullSize = false,
shouldContainScroll = true,
isGroupPolicyReport = false,
showSoftInputOnFocus = true,
...props
}: ComposerProps,
ref: ForwardedRef<TextInput | HTMLInputElement>,
Expand All @@ -74,6 +75,11 @@ function Composer(
});
const [hasMultipleLines, setHasMultipleLines] = useState(false);
const [isRendered, setIsRendered] = useState(false);

// On mobile safari, the cursor will move from right to left with inputMode set to none during report transition
// To avoid that we should hide the cursor util the transition is finished
const [shouldTransparentCursor, setShouldTransparentCursor] = useState(!showSoftInputOnFocus && Browser.isMobileSafari());

const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? '');
const [prevScroll, setPrevScroll] = useState<number | undefined>();
const [prevHeight, setPrevHeight] = useState<number | undefined>();
Expand Down Expand Up @@ -260,6 +266,15 @@ function Composer(
setIsRendered(true);
}, []);

useEffect(() => {
if (!shouldTransparentCursor) {
return;
}
InteractionManager.runAfterInteractions(() => {
setShouldTransparentCursor(false);
});
}, [shouldTransparentCursor]);

const clear = useCallback(() => {
if (!textInput.current) {
return;
Expand Down Expand Up @@ -347,11 +362,12 @@ function Composer(
placeholderTextColor={theme.placeholderText}
ref={(el) => (textInput.current = el)}
selection={selection}
style={[inputStyleMemo]}
style={[inputStyleMemo, shouldTransparentCursor ? {caretColor: 'transparent'} : undefined]}
markdownStyle={markdownStyle}
value={value}
defaultValue={defaultValue}
autoFocus={autoFocus}
inputMode={showSoftInputOnFocus ? 'text' : 'none'}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
onSelectionChange={addCursorPositionToSelectionChange}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Composer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ type ComposerProps = Omit<TextInputProps, 'onClear'> & {

/** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */
isGroupPolicyReport?: boolean;

/** Whether to show the keyboard on focus */
showSoftInputOnFocus?: boolean;
};

export type {TextSelection, ComposerProps, CustomSelectionChangeEvent};
4 changes: 2 additions & 2 deletions src/libs/actions/EmojiPickerAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ function hideEmojiPicker(isNavigating?: boolean) {
/**
* Whether Emoji Picker is active for the given id.
*/
function isActive(id: string): boolean {
if (!emojiPickerRef.current) {
function isActive(id?: string): boolean {
if (!emojiPickerRef.current || !id) {
return false;
}

Expand Down
7 changes: 4 additions & 3 deletions src/pages/home/ReportScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro

const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report);
const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]);
const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
const indexOfLinkedMessage = useMemo(
(): number => reportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)),
Expand Down Expand Up @@ -282,6 +281,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`];
const isTopMostReportId = currentReportID === reportIDFromRoute;
const didSubscribeToReportLeavingEvents = useRef(false);
const [showSoftInputOnFocus, setShowSoftInputOnFocus] = useState(false);

useEffect(() => {
if (!report?.reportID || shouldHideReport) {
Expand Down Expand Up @@ -759,7 +759,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
<ScreenWrapper
navigation={navigation}
style={screenWrapperStyle}
shouldEnableKeyboardAvoidingView={isTopMostReportId || isInNarrowPaneModal}
shouldEnableKeyboardAvoidingView={(isTopMostReportId || isInNarrowPaneModal) && (!isComposerFocus || showSoftInputOnFocus)}
testID={`report-screen-${reportID ?? ''}`}
>
<FullPageNotFoundView
Expand Down Expand Up @@ -856,8 +856,9 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
policy={policy}
pendingAction={reportPendingAction}
isComposerFullSize={!!isComposerFullSize}
isEmptyChat={isEmptyChat}
lastReportAction={lastReportAction}
showSoftInputOnFocus={showSoftInputOnFocus}
setShowSoftInputOnFocus={setShowSoftInputOnFocus}
workspaceTooltip={workspaceTooltip}
/>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import getPlatform from '@libs/getPlatform';
import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener';
import Parser from '@libs/Parser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside';
Expand Down Expand Up @@ -126,27 +125,26 @@ type ComposerWithSuggestionsProps = Partial<ChildrenProps> & {
/** The ref to the next modal will open */
isNextModalWillOpenRef: MutableRefObject<boolean | null>;

/** Wheater chat is empty */
isEmptyChat?: boolean;

/** The last report action */
lastReportAction?: OnyxEntry<OnyxTypes.ReportAction>;

/** Whether to include chronos */
includeChronos?: boolean;

/** The parent report action ID */
parentReportActionID?: string;

/** The parent report ID */
// eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC
parentReportID: string | undefined;

/** Whether report is from group policy */
isGroupPolicyReport: boolean;

/** policy ID of the report */
policyID: string;
policyID?: string;

/** Whether to show the keyboard on focus */
showSoftInputOnFocus: boolean;

/** A method to update showSoftInputOnFocus */
setShowSoftInputOnFocus: (value: boolean) => void;

/** Whether the main composer was hidden */
didHideComposerInput?: boolean;
};

type SwitchToCurrentReportProps = {
Expand Down Expand Up @@ -187,10 +185,6 @@ const debouncedBroadcastUserIsTyping = lodashDebounce(

const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc();

// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
// prevent auto focus on existing chat for mobile device
const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();

/**
* This component holds the value and selection state.
* If a component really needs access to these state values it should be put here.
Expand All @@ -201,11 +195,8 @@ function ComposerWithSuggestions(
{
// Props: Report
reportID,
parentReportID,
includeChronos,
isEmptyChat,
lastReportAction,
parentReportActionID,
isGroupPolicyReport,
policyID,

Expand Down Expand Up @@ -236,6 +227,9 @@ function ComposerWithSuggestions(

// For testing
children,
showSoftInputOnFocus,
setShowSoftInputOnFocus,
didHideComposerInput,
}: ComposerWithSuggestionsProps,
ref: ForwardedRef<ComposerRef>,
) {
Expand All @@ -257,14 +251,12 @@ function ComposerWithSuggestions(
}
return draftComment;
});

const commentRef = useRef(value);

const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
const [modal] = useOnyx(ONYXKEYS.MODAL);
const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: EmojiUtils.getPreferredSkinToneIndex});
const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID || '-1'}`, {canEvict: false, initWithStoredValues: false});

const lastTextRef = useRef(value);
useEffect(() => {
Expand All @@ -274,13 +266,7 @@ function ComposerWithSuggestions(
const {shouldUseNarrowLayout} = useResponsiveLayout();
const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES;

const parentReportAction = useMemo(() => parentReportActions?.[parentReportActionID ?? '-1'], [parentReportActionID, parentReportActions]);
const shouldAutoFocus =
!modal?.isVisible &&
Modal.areAllModalsHidden() &&
isFocused &&
(shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction) && !ReportUtils.isTaskReport(report))) &&
shouldShowComposeInput;
const shouldAutoFocus = !modal?.isVisible && shouldShowComposeInput && Modal.areAllModalsHidden() && isFocused && !didHideComposerInput;

const valueRef = useRef(value);
valueRef.current = value;
Expand Down Expand Up @@ -643,7 +629,15 @@ function ComposerWithSuggestions(
// We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused.
// We avoid doing this on native platforms since the software keyboard popping
// open creates a jarring and broken UX.
if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal?.isVisible && isFocused && (!!prevIsModalVisible || !prevIsFocused))) {
if (
!(
(willBlurTextInputOnTapOutside || (shouldAutoFocus && canFocusInputOnScreenFocus())) &&
!isNextModalWillOpenRef.current &&
!modal?.isVisible &&
isFocused &&
(!!prevIsModalVisible || !prevIsFocused)
)
) {
return;
}

Expand Down Expand Up @@ -775,6 +769,19 @@ function ComposerWithSuggestions(
onScroll={hideSuggestionMenu}
shouldContainScroll={Browser.isMobileSafari()}
isGroupPolicyReport={isGroupPolicyReport}
showSoftInputOnFocus={showSoftInputOnFocus}
onTouchStart={() => {
if (showSoftInputOnFocus) {
return;
}
if (Browser.isMobileSafari()) {
setTimeout(() => {
setShowSoftInputOnFocus(true);
}, CONST.ANIMATED_TRANSITION);
return;
}
setShowSoftInputOnFocus(true);
}}
/>
</View>

Expand Down
25 changes: 18 additions & 7 deletions src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ type SuggestionsRef = {
getIsSuggestionsMenuVisible: () => boolean;
};

type ReportActionComposeProps = Pick<ComposerWithSuggestionsProps, 'reportID' | 'isEmptyChat' | 'isComposerFullSize' | 'lastReportAction'> & {
type ReportActionComposeProps = Pick<ComposerWithSuggestionsProps, 'reportID' | 'isComposerFullSize' | 'lastReportAction'> & {
/** A method to call when the form is submitted */
onSubmit: (newComment: string) => void;

Expand All @@ -91,6 +91,15 @@ type ReportActionComposeProps = Pick<ComposerWithSuggestionsProps, 'reportID' |

/** Should show educational tooltip */
shouldShowEducationalTooltip?: boolean;

/** Whether to show the keyboard on focus */
showSoftInputOnFocus: boolean;

/** A method to update showSoftInputOnFocus */
setShowSoftInputOnFocus: (value: boolean) => void;

/** Whether the main composer was hidden */
didHideComposerInput?: boolean;
};

// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
Expand All @@ -110,11 +119,13 @@ function ReportActionCompose({
report,
reportID,
isReportReadyForDisplay = true,
isEmptyChat,
lastReportAction,
shouldShowEducationalTooltip,
showSoftInputOnFocus,
onComposerFocus,
onComposerBlur,
setShowSoftInputOnFocus,
didHideComposerInput,
}: ReportActionComposeProps) {
const theme = useTheme();
const styles = useThemeStyles();
Expand Down Expand Up @@ -323,7 +334,7 @@ function ReportActionCompose({
// We are returning a callback here as we want to incoke the method on unmount only
useEffect(
() => () => {
if (!EmojiPickerActions.isActive(report?.reportID ?? '-1')) {
if (!EmojiPickerActions.isActive(report?.reportID)) {
return;
}
EmojiPickerActions.hideEmojiPicker();
Expand Down Expand Up @@ -514,12 +525,9 @@ function ReportActionCompose({
isScrollLikelyLayoutTriggered={isScrollLikelyLayoutTriggered}
raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered}
reportID={reportID}
policyID={report?.policyID ?? '-1'}
parentReportID={report?.parentReportID}
parentReportActionID={report?.parentReportActionID}
policyID={report?.policyID}
includeChronos={ReportUtils.chatIncludesChronos(report)}
isGroupPolicyReport={isGroupPolicyReport}
isEmptyChat={isEmptyChat}
lastReportAction={lastReportAction}
isMenuVisible={isMenuVisible}
inputPlaceholder={inputPlaceholder}
Expand All @@ -534,7 +542,10 @@ function ReportActionCompose({
onFocus={onFocus}
onBlur={onBlur}
measureParentContainer={measureContainer}
showSoftInputOnFocus={showSoftInputOnFocus}
setShowSoftInputOnFocus={setShowSoftInputOnFocus}
onValueChange={onValueChange}
didHideComposerInput={didHideComposerInput}
/>
<ReportDropUI
onDrop={(event: DragEvent) => {
Expand Down
Loading

0 comments on commit 8819d9c

Please sign in to comment.