Skip to content

Commit

Permalink
Merge pull request #181 from software-mansion-labs/kicu/fix-composer-…
Browse files Browse the repository at this point in the history
…focusing

Fix composer focusing issues
  • Loading branch information
Kicu authored Jan 29, 2025
2 parents 225ca63 + b4ab2d3 commit 0e43795
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {useRoute} from '@react-navigation/native';
import React, {useState} from 'react';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import usePermissions from '@hooks/usePermissions';
import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
Expand Down Expand Up @@ -38,26 +37,24 @@ function ReportsSplitNavigator() {

return (
<FreezeWrapper>
<FocusTrapForScreens>
<Split.Navigator
persistentScreens={[SCREENS.HOME]}
sidebarScreen={SCREENS.HOME}
defaultCentralScreen={SCREENS.REPORT}
parentRoute={route}
screenOptions={splitNavigatorScreenOptions.centralScreen}
>
<Split.Screen
name={SCREENS.HOME}
getComponent={loadSidebarScreen}
options={splitNavigatorScreenOptions.sidebarScreen}
/>
<Split.Screen
name={SCREENS.REPORT}
initialParams={{reportID: initialReportID, openOnAdminRoom: shouldOpenOnAdminRoom() ? true : undefined}}
getComponent={loadReportScreen}
/>
</Split.Navigator>
</FocusTrapForScreens>
<Split.Navigator
persistentScreens={[SCREENS.HOME]}
sidebarScreen={SCREENS.HOME}
defaultCentralScreen={SCREENS.REPORT}
parentRoute={route}
screenOptions={splitNavigatorScreenOptions.centralScreen}
>
<Split.Screen
name={SCREENS.HOME}
getComponent={loadSidebarScreen}
options={splitNavigatorScreenOptions.sidebarScreen}
/>
<Split.Screen
name={SCREENS.REPORT}
initialParams={{reportID: initialReportID, openOnAdminRoom: shouldOpenOnAdminRoom() ? true : undefined}}
getComponent={loadReportScreen}
/>
</Split.Navigator>
</FreezeWrapper>
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/libs/ReportActionComposeFocusManager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {findFocusedRoute} from '@react-navigation/native';
import React from 'react';
import type {MutableRefObject} from 'react';
import type {TextInput} from 'react-native';
import SCREENS from '@src/SCREENS';
import getTopmostRouteName from './Navigation/helpers/getTopmostRouteName';
import isReportOpenInRHP from './Navigation/helpers/isReportOpenInRHP';
import navigationRef from './Navigation/navigationRef';

Expand Down Expand Up @@ -38,7 +38,8 @@ function onComposerFocus(callback: FocusCallback | null, isPriorityCallback = fa
function focus(shouldFocusForNonBlurInputOnTapOutside?: boolean) {
/** Do not trigger the refocusing when the active route is not the report screen */
const navigationState = navigationRef.getState();
if (!navigationState || (!isReportOpenInRHP(navigationState) && getTopmostRouteName(navigationState) !== SCREENS.REPORT)) {
const focusedRoute = findFocusedRoute(navigationState);
if (!navigationState || (!isReportOpenInRHP(navigationState) && focusedRoute?.name !== SCREENS.REPORT)) {
return;
}

Expand Down
9 changes: 6 additions & 3 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1179,10 +1179,13 @@ function navigateToAndOpenReport(
// We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
openReport(report?.reportID, '', userLogins, newChat, undefined, undefined, undefined, avatarFile);
if (shouldDismissModal) {
Navigation.dismissModalWithReport(report);
return;
Navigation.dismissModal();
}
Navigation.navigateToReportWithPolicyCheck({report});

// In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks, wrapping navigation in setTimeout fixes this
setTimeout(() => {
Navigation.isNavigationReady().then(() => Navigation.navigateToReportWithPolicyCheck({report}));
}, 0);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/pages/home/report/PureReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ function PureReportActionItem({
const [moderationDecision, setModerationDecision] = useState<OnyxTypes.DecisionName>(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
const reactionListRef = useContext(ReactionListContext);
const {updateHiddenAttachments} = useContext(ReportAttachmentsContext);
const textInputRef = useRef<TextInput | HTMLTextAreaElement>(null);
const composerTextInputRef = useRef<TextInput | HTMLTextAreaElement>(null);
const popoverAnchorRef = useRef<Exclude<ContextMenuAnchor, TextInput>>(null);
const downloadedPreviews = useRef<string[]>([]);
const prevDraftMessage = usePrevious(draftMessage);
Expand Down Expand Up @@ -443,7 +443,7 @@ function PureReportActionItem({
return;
}

focusComposerWithDelay(textInputRef.current)(true);
focusComposerWithDelay(composerTextInputRef.current)(true);
}, [prevDraftMessage, draftMessage]);

useEffect(() => {
Expand Down Expand Up @@ -951,7 +951,7 @@ function PureReportActionItem({
reportID={reportID}
policyID={report?.policyID}
index={index}
ref={textInputRef}
ref={composerTextInputRef}
shouldDisableEmojiPicker={
(chatIncludesConcierge(report) && isBlockedFromConcierge(blockedFromConcierge)) || isArchivedNonExpenseReport(report, reportNameValuePairs)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -646,14 +646,16 @@ function ComposerWithSuggestions(
const prevIsFocused = usePrevious(isFocused);

useEffect(() => {
if (modal?.isVisible && !prevIsModalVisible) {
const isModalVisible = modal?.isVisible;
if (isModalVisible && !prevIsModalVisible) {
// eslint-disable-next-line react-compiler/react-compiler, no-param-reassign
isNextModalWillOpenRef.current = false;
}

// We want to blur the input immediately when a screen is out of focus.
if (!isFocused) {
textInputRef.current?.blur();
return;
}

// We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused.
Expand All @@ -663,8 +665,7 @@ function ComposerWithSuggestions(
!(
(willBlurTextInputOnTapOutside || (shouldAutoFocus && canFocusInputOnScreenFocus())) &&
!isNextModalWillOpenRef.current &&
!modal?.isVisible &&
isFocused &&
!isModalVisible &&
(!!prevIsModalVisible || !prevIsFocused)
)
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ function ReportActionCompose({
reportParticipantIDs={reportParticipantIDs}
isComposerFullSize={isComposerFullSize}
isBlockedFromConcierge={isBlockedFromConcierge}
disabled={!!disabled}
disabled={disabled}
setMenuVisibility={setMenuVisibility}
isMenuVisible={isMenuVisible}
onTriggerAttachmentPicker={onTriggerAttachmentPicker}
Expand Down Expand Up @@ -505,7 +505,7 @@ function ReportActionCompose({
displayFileInModal={displayFileInModal}
onCleared={submitForm}
isBlockedFromConcierge={isBlockedFromConcierge}
disabled={!!disabled}
disabled={disabled}
setIsCommentEmpty={setIsCommentEmpty}
handleSendMessage={handleSendMessage}
shouldShowComposeInput={shouldShowComposeInput}
Expand Down
79 changes: 11 additions & 68 deletions src/pages/home/report/ReportActionItemMessageEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,14 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {setShouldShowComposeInput} from '@libs/actions/Composer';
import {clearActive, isActive as isEmojiPickerActive, isEmojiPickerVisible} from '@libs/actions/EmojiPickerAction';
import {composerFocusKeepFocusOn, callback as inputFocusCallback, inputFocusChange} from '@libs/actions/InputFocus';
import {composerFocusKeepFocusOn} from '@libs/actions/InputFocus';
import {deleteReportActionDraft, editReportComment, saveReportActionDraft} from '@libs/actions/Report';
import {canSkipTriggerHotkeys, insertText} from '@libs/ComposerUtils';
import DomUtils from '@libs/DomUtils';
import {extractEmojis, replaceAndExtractEmojis} from '@libs/EmojiUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import type {Selection} from '@libs/focusComposerWithDelay/types';
import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
import onyxSubscribe from '@libs/onyxSubscribe';
import Parser from '@libs/Parser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import reportActionItemEventHandler from '@libs/ReportActionItemEventHandler';
Expand Down Expand Up @@ -98,7 +97,6 @@ function ReportActionItemMessageEdit(
const mobileInputScrollPosition = useRef(0);
const cursorPositionValue = useSharedValue({x: 0, y: 0});
const tag = useSharedValue(-1);
const isInitialMount = useRef(true);
const emojisPresentBefore = useRef<Emoji[]>([]);
const [draft, setDraft] = useState(() => {
if (draftMessage) {
Expand All @@ -110,11 +108,14 @@ function ReportActionItemMessageEdit(
const [isFocused, setIsFocused] = useState<boolean>(false);
const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength();
const debouncedValidateCommentMaxLength = useMemo(() => lodashDebounce(validateCommentMaxLength, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME), [validateCommentMaxLength]);
const [modal, setModal] = useState<OnyxTypes.Modal>({
willAlertModalBecomeVisible: false,
isVisible: false,
});
const [onyxFocused, setOnyxFocused] = useState<boolean>(false);

const [
modal = {
willAlertModalBecomeVisible: false,
isVisible: false,
},
] = useOnyx(ONYXKEYS.MODAL);
const [onyxInputFocused = false] = useOnyx(ONYXKEYS.INPUT_FOCUSED);

const textInputRef = useRef<(HTMLTextAreaElement & TextInput) | null>(null);
const isFocusedRef = useRef<boolean>(false);
Expand All @@ -136,34 +137,8 @@ function ReportActionItemMessageEdit(
}, [draftMessage, action, prevDraftMessage]);

useEffect(() => {
composerFocusKeepFocusOn(textInputRef.current as HTMLElement, isFocused, modal, onyxFocused);
}, [isFocused, modal, onyxFocused]);

useEffect(() => {
const unsubscribeOnyxModal = onyxSubscribe({
key: ONYXKEYS.MODAL,
callback: (modalArg) => {
if (modalArg === undefined) {
return;
}
setModal(modalArg);
},
});

const unsubscribeOnyxFocused = onyxSubscribe({
key: ONYXKEYS.INPUT_FOCUSED,
callback: (modalArg) => {
if (modalArg === undefined) {
return;
}
setOnyxFocused(modalArg);
},
});
return () => {
unsubscribeOnyxModal();
unsubscribeOnyxFocused();
};
}, []);
composerFocusKeepFocusOn(textInputRef.current as HTMLElement, isFocused, modal, onyxInputFocused);
}, [isFocused, modal, onyxInputFocused]);

useEffect(
// Remove focus callback on unmount to avoid stale callbacks
Expand Down Expand Up @@ -202,38 +177,6 @@ function ReportActionItemMessageEdit(
}, true);
}, [focus]);

useEffect(
() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}

return () => {
inputFocusCallback(() => setIsFocused(false));
inputFocusChange(false);

// Skip if the current report action is not active
if (!isActive()) {
return;
}

if (isEmojiPickerActive(action.reportActionID)) {
clearActive();
}
if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) {
ReportActionContextMenu.clearActiveReportAction();
}

// Show the main composer when the focused message is deleted from another client
// to prevent the main composer stays hidden until we switch to another chat.
setShouldShowComposeInputKeyboardAware(true);
};
},
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this cleanup needs to be called only on unmount
[action.reportActionID],
);

// show the composer after editing is complete for devices that hide the composer during editing.
useEffect(() => () => setShouldShowComposeInput(true), []);

Expand Down

0 comments on commit 0e43795

Please sign in to comment.