Skip to content

Commit

Permalink
Merge pull request #37666 from MrRefactor/proposal/28563
Browse files Browse the repository at this point in the history
Fix problem with cursor jumping back to start when adding an emoji in edit mode
  • Loading branch information
neil-marcellini authored Mar 25, 2024
2 parents 3f525d9 + 734ded0 commit 3dd3f6b
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import type {TextInput} from 'react-native';
import * as EmojiPickerAction from './actions/EmojiPickerAction';
import ComposerFocusManager from './ComposerFocusManager';
import ComposerFocusManager from '@libs/ComposerFocusManager';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
import setTextInputSelection from './setTextInputSelection';
import type {FocusComposerWithDelay, InputType} from './types';

type FocusComposerWithDelay = (shouldDelay?: boolean) => void;
/**
* Create a function that focuses the composer.
*/
function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithDelay {
function focusComposerWithDelay(textInput: InputType | null): FocusComposerWithDelay {
/**
* Focus the text input
* @param [shouldDelay] Impose delay before focusing the text input
* @param [forcedSelectionRange] Force selection range of text input
*/
return (shouldDelay = false) => {
return (shouldDelay = false, forcedSelectionRange = undefined) => {
// There could be other animations running while we trigger manual focus.
// This prevents focus from making those animations janky.
if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) {
Expand All @@ -20,13 +21,19 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD

if (!shouldDelay) {
textInput.focus();
if (forcedSelectionRange) {
setTextInputSelection(textInput, forcedSelectionRange);
}
return;
}
ComposerFocusManager.isReadyToFocus().then(() => {
if (!textInput) {
return;
}
textInput.focus();
if (forcedSelectionRange) {
setTextInputSelection(textInput, forcedSelectionRange);
}
});
};
}
Expand Down
15 changes: 15 additions & 0 deletions src/libs/focusComposerWithDelay/setTextInputSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {TextInput} from 'react-native';
import shouldSetSelectionRange from '@libs/shouldSetSelectionRange';
import type {InputType, Selection} from './types';

const setSelectionRange = shouldSetSelectionRange();

const setTextInputSelection = (textInput: InputType, forcedSelectionRange: Selection) => {
if (setSelectionRange) {
(textInput as HTMLTextAreaElement).setSelectionRange(forcedSelectionRange.start, forcedSelectionRange.end);
} else {
(textInput as TextInput).setSelection(forcedSelectionRange.start, forcedSelectionRange.end);
}
};

export default setTextInputSelection;
12 changes: 12 additions & 0 deletions src/libs/focusComposerWithDelay/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {TextInput} from 'react-native';

type Selection = {
start: number;
end: number;
};

type FocusComposerWithDelay = (shouldDelay?: boolean, forcedSelectionRange?: Selection) => void;

type InputType = TextInput | HTMLTextAreaElement;

export type {Selection, FocusComposerWithDelay, InputType};
5 changes: 5 additions & 0 deletions src/libs/shouldSetSelectionRange/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type ShouldSetSelectionRange from './types';

const shouldSetSelectionRange: ShouldSetSelectionRange = () => false;

export default shouldSetSelectionRange;
5 changes: 5 additions & 0 deletions src/libs/shouldSetSelectionRange/index.website.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type ShouldSetSelectionRange from './types';

const shouldSetSelectionRange: ShouldSetSelectionRange = () => true;

export default shouldSetSelectionRange;
3 changes: 3 additions & 0 deletions src/libs/shouldSetSelectionRange/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type ShouldSetSelectionRange = () => boolean;

export default ShouldSetSelectionRange;
28 changes: 19 additions & 9 deletions src/pages/home/report/ReportActionItemMessageEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as Browser from '@libs/Browser';
import * as ComposerUtils from '@libs/ComposerUtils';
import * as EmojiUtils 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 ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
Expand All @@ -42,6 +43,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection';

type ReportActionItemMessageEditProps = {
/** All the data of the action */
Expand All @@ -68,6 +70,7 @@ const emojiButtonID = 'emojiButton';
const messageEditInput = 'messageEditInput';

const isMobileSafari = Browser.isMobileSafari();
const shouldUseForcedSelectionRange = shouldUseEmojiPickerSelection();

function ReportActionItemMessageEdit(
{action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps,
Expand Down Expand Up @@ -108,10 +111,7 @@ function ReportActionItemMessageEdit(
}
return initialDraft;
});
const [selection, setSelection] = useState<{
start: number;
end: number;
}>(getInitialSelection);
const [selection, setSelection] = useState<Selection>(getInitialSelection);
const [isFocused, setIsFocused] = useState<boolean>(false);
const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength();
const [modal, setModal] = useState<OnyxTypes.Modal>({
Expand All @@ -124,6 +124,7 @@ function ReportActionItemMessageEdit(
const isFocusedRef = useRef<boolean>(false);
const insertedEmojis = useRef<Emoji[]>([]);
const draftRef = useRef(draft);
const emojiPickerSelectionRef = useRef<Selection | undefined>(undefined);

useEffect(() => {
if (ReportActionsUtils.isDeletedAction(action) || Boolean(action.message && draftMessage === action.message[0].html) || Boolean(prevDraftMessage === draftMessage)) {
Expand Down Expand Up @@ -336,10 +337,17 @@ function ReportActionItemMessageEdit(
* @param emoji
*/
const addEmojiToTextBox = (emoji: string) => {
setSelection((prevSelection) => ({
start: prevSelection.start + emoji.length + CONST.SPACE_LENGTH,
end: prevSelection.start + emoji.length + CONST.SPACE_LENGTH,
}));
const newSelection = {
start: selection.start + emoji.length + CONST.SPACE_LENGTH,
end: selection.start + emoji.length + CONST.SPACE_LENGTH,
};
setSelection(newSelection);

if (shouldUseForcedSelectionRange) {
// On Android and Chrome mobile, focusing the input sets the cursor position back to the start.
// To fix this, immediately set the selection again after focusing the input.
emojiPickerSelectionRef.current = newSelection;
}
updateDraft(ComposerUtils.insertText(draft, selection, `${emoji} `));
};

Expand Down Expand Up @@ -453,7 +461,9 @@ function ReportActionItemMessageEdit(
<View style={styles.editChatItemEmojiWrapper}>
<EmojiPickerButton
isDisabled={shouldDisableEmojiPicker}
onModalHide={() => focus(true)}
onModalHide={() => {
focus(true, emojiPickerSelectionRef.current ? {...emojiPickerSelectionRef.current} : undefined);
}}
onEmojiSelected={addEmojiToTextBox}
id={emojiButtonID}
emojiPickerID={action.reportActionID}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type ShouldUseEmojiPickerSelection from './types';

const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => true;

export default shouldUseEmojiPickerSelection;
5 changes: 5 additions & 0 deletions src/pages/home/report/shouldUseEmojiPickerSelection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type ShouldUseEmojiPickerSelection from './types';

const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => false;

export default shouldUseEmojiPickerSelection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Browser from '@libs/Browser';
import type ShouldUseEmojiPickerSelection from './types';

const isMobileChrome = Browser.isMobileChrome();

const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => isMobileChrome;

export default shouldUseEmojiPickerSelection;
3 changes: 3 additions & 0 deletions src/pages/home/report/shouldUseEmojiPickerSelection/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type ShouldUseEmojiPickerSelection = () => boolean;

export default ShouldUseEmojiPickerSelection;

0 comments on commit 3dd3f6b

Please sign in to comment.