Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

16098 — selecting chat message and pressing any key focus the message instead of switch the focus to the composer #21583

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
82fbeb0
fix parseReportRouteParams so it can be used with getActiveRoute func…
robertKozik Jun 22, 2023
81695ef
create KeyDownPress listner lib
robertKozik Jun 22, 2023
4764004
create isEmojiPickerVisible function
robertKozik Jun 22, 2023
0a5873c
add keyDown and paste listners docuemnt-scope in order to focus composer
robertKozik Jun 22, 2023
32e021f
Merge branch 'main' into 16098-selecting-chat-message-and-pressing-an…
robertKozik Jun 28, 2023
6eea6aa
manage listeners on focus/blur basis
robertKozik Jun 29, 2023
28240d8
prettier
robertKozik Jun 29, 2023
8270068
remove unused imports
robertKozik Jun 29, 2023
8e453c7
Merge remote-tracking branch 'upstream/main' into 16098-selecting-cha…
robertKozik Jun 29, 2023
8ae5a56
add emojiPicker visiblity state to imperative handle
robertKozik Jun 29, 2023
e979d2b
conditionally add trailing space in the replaceSelectionWithText method
robertKozik Jul 4, 2023
42e6bc8
Merge branch 'main' into 16098-selecting-chat-message-and-pressing-an…
robertKozik Jul 4, 2023
3d12ed7
refactor navigation unsubscribers names
robertKozik Jul 4, 2023
99edfa1
rephrase comment to me more precise
robertKozik Jul 5, 2023
889dd18
move FAB ref to separate file, create function to check create menu v…
robertKozik Jul 6, 2023
eb26193
Consider FAB popover menu status while checking composer visibility
robertKozik Jul 6, 2023
834fb72
prettier
robertKozik Jul 6, 2023
1f4bf0b
remove unused useRef import
robertKozik Jul 6, 2023
2eac511
move FloatingActionButtonAndPopoverUtils to libs directory
robertKozik Jul 7, 2023
8b49b86
detect popover status based on modal isVisible onyx state
robertKozik Jul 10, 2023
93654fc
Merge branch 'main' into 16098-selecting-chat-message-and-pressing-an…
robertKozik Jul 10, 2023
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
10 changes: 8 additions & 2 deletions src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,17 @@ export default {
* @returns {Object}
*/
parseReportRouteParams: (route) => {
if (!route.startsWith(Url.addTrailingForwardSlash(REPORT))) {
let parsingRoute = route;
if (parsingRoute.at(0) === '/') {
// remove the first slash
parsingRoute = parsingRoute.slice(1);
}

if (!parsingRoute.startsWith(Url.addTrailingForwardSlash(REPORT))) {
return {reportID: '', isSubReportPageRoute: false};
}

const pathSegments = route.split('/');
const pathSegments = parsingRoute.split('/');
return {
reportID: lodashGet(pathSegments, 1),
isSubReportPageRoute: Boolean(lodashGet(pathSegments, 2)),
Expand Down
26 changes: 24 additions & 2 deletions src/components/Composer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import styles from '../../styles/styles';
import Text from '../Text';
import isEnterWhileComposition from '../../libs/KeyboardShortcut/isEnterWhileComposition';
import CONST from '../../CONST';
import withNavigation from '../withNavigation';

const propTypes = {
/** Maximum number of lines in the text input */
Expand Down Expand Up @@ -77,6 +78,9 @@ const propTypes = {
/** Should we calculate the caret position */
shouldCalculateCaretPosition: PropTypes.bool,

/** Function to check whether composer is covered up or not */
checkComposerVisibility: PropTypes.func,

...withLocalizePropTypes,

...windowDimensionsPropTypes,
Expand Down Expand Up @@ -104,6 +108,7 @@ const defaultProps = {
setIsFullComposerAvailable: () => {},
isComposerFullSize: false,
shouldCalculateCaretPosition: false,
checkComposerVisibility: () => false,
};

const IMAGE_EXTENSIONS = {
Expand Down Expand Up @@ -143,6 +148,8 @@ class Composer extends React.Component {
this.shouldCallUpdateNumberOfLines = this.shouldCallUpdateNumberOfLines.bind(this);
this.addCursorPositionToSelectionChange = this.addCursorPositionToSelectionChange.bind(this);
this.textRef = React.createRef(null);
this.unsubscribeBlur = () => null;
this.unsubscribeFocus = () => null;
}

componentDidMount() {
Expand All @@ -159,8 +166,15 @@ class Composer extends React.Component {
// There is no onPaste or onDrag for TextInput in react-native so we will add event
// listeners here and unbind when the component unmounts
if (this.textInput) {
this.textInput.addEventListener('paste', this.handlePaste);
this.textInput.addEventListener('wheel', this.handleWheel);

// we need to handle listeners on navigation focus/blur as Composer is not unmounting
// when navigating away to different report
this.unsubscribeFocus = this.props.navigation.addListener('focus', () => document.addEventListener('paste', this.handlePaste));
this.unsubscribeBlur = this.props.navigation.addListener('blur', () => document.removeEventListener('paste', this.handlePaste));

// We need to add paste listener manually as well as navigation focus event is not triggered on component mount
document.addEventListener('paste', this.handlePaste);
}
}

Expand Down Expand Up @@ -193,7 +207,9 @@ class Composer extends React.Component {
return;
}

this.textInput.removeEventListener('paste', this.handlePaste);
document.removeEventListener('paste', this.handlePaste);
this.unsubscribeFocus();
this.unsubscribeBlur();
this.textInput.removeEventListener('wheel', this.handleWheel);
}

Expand Down Expand Up @@ -262,6 +278,7 @@ class Composer extends React.Component {
*/
paste(text) {
try {
this.textInput.focus();
document.execCommand('insertText', false, text);
this.updateNumberOfLines();

Expand Down Expand Up @@ -289,6 +306,10 @@ class Composer extends React.Component {
* @param {ClipboardEvent} event
*/
handlePaste(event) {
if (!this.props.checkComposerVisibility() && !this.state.isFocused) {
return;
}

event.preventDefault();

const {files, types} = event.clipboardData;
Expand Down Expand Up @@ -474,6 +495,7 @@ Composer.defaultProps = defaultProps;
export default compose(
withLocalize,
withWindowDimensions,
withNavigation,
)(
React.forwardRef((props, ref) => (
<Composer
Expand Down
2 changes: 1 addition & 1 deletion src/components/EmojiPicker/EmojiPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const EmojiPicker = forwardRef((props, ref) => {
*/
const isActiveReportAction = (actionID) => Boolean(actionID) && reportAction.reportActionID === actionID;

useImperativeHandle(ref, () => ({showEmojiPicker, isActiveReportAction, hideEmojiPicker}));
useImperativeHandle(ref, () => ({showEmojiPicker, isActiveReportAction, hideEmojiPicker, isEmojiPickerVisible}));

// There is no way to disable animations, and they are really laggy, because there are so many
// emojis. The best alternative is to set it to 1ms so it just "pops" in and out
Expand Down
9 changes: 9 additions & 0 deletions src/libs/KeyboardShortcut/KeyDownPressListener/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function addKeyDownPressListner(callbackFunction) {
document.addEventListener('keydown', callbackFunction);
}

function removeKeyDownPressListner(callbackFunction) {
document.removeEventListener('keydown', callbackFunction);
}

export {addKeyDownPressListner, removeKeyDownPressListner};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function addKeyDownPressListner() {}
function removeKeyDownPressListner() {}

export {addKeyDownPressListner, removeKeyDownPressListner};
9 changes: 8 additions & 1 deletion src/libs/actions/EmojiPickerAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,11 @@ function isActiveReportAction(actionID) {
return emojiPickerRef.current.isActiveReportAction(actionID);
}

export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActiveReportAction};
function isEmojiPickerVisible() {
if (!emojiPickerRef.current) {
return;
}
return emojiPickerRef.current.isEmojiPickerVisible;
}

export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActiveReportAction, isEmojiPickerVisible};
72 changes: 63 additions & 9 deletions src/pages/home/report/ReportActionCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import * as Task from '../../../libs/actions/Task';
import * as Browser from '../../../libs/Browser';
import * as IOU from '../../../libs/actions/IOU';
import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback';
import * as KeyDownListener from '../../../libs/KeyboardShortcut/KeyDownPressListener';
import * as EmojiPickerActions from '../../../libs/actions/EmojiPickerAction';

const propTypes = {
/** Beta features list */
Expand Down Expand Up @@ -168,7 +170,9 @@ class ReportActionCompose extends React.Component {
this.setIsFocused = this.setIsFocused.bind(this);
this.setIsFullComposerAvailable = this.setIsFullComposerAvailable.bind(this);
this.focus = this.focus.bind(this);
this.addEmojiToTextBox = this.addEmojiToTextBox.bind(this);
this.replaceSelectionWithText = this.replaceSelectionWithText.bind(this);
this.focusComposerOnKeyPress = this.focusComposerOnKeyPress.bind(this);
this.checkComposerVisibility = this.checkComposerVisibility.bind(this);
this.onSelectionChange = this.onSelectionChange.bind(this);
this.isEmojiCode = this.isEmojiCode.bind(this);
this.isMentionCode = this.isMentionCode.bind(this);
Expand All @@ -186,6 +190,8 @@ class ReportActionCompose extends React.Component {
this.comment = props.comment;
this.insertedEmojis = [];

this.attachmentModalRef = React.createRef();

// React Native will retain focus on an input for native devices but web/mWeb behave differently so we have some focus management
// code that will refocus the compose input after a user closes a modal or some other actions, see usage of ReportActionComposeFocusManager
this.willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutside();
Expand All @@ -205,6 +211,9 @@ class ReportActionCompose extends React.Component {
// so we need to ensure that it is only updated after focus.
const isMobileSafari = Browser.isMobileSafari();

this.unsubscribeNavigationBlur = () => null;
this.unsubscribeNavigationFocus = () => null;

this.state = {
isFocused: this.shouldFocusInputOnScreenFocus && !this.props.modal.isVisible && !this.props.modal.willAlertModalBecomeVisible && this.props.shouldShowComposeInput,
isFullComposerAvailable: props.isComposerFullSize,
Expand Down Expand Up @@ -238,6 +247,10 @@ class ReportActionCompose extends React.Component {
this.focus(false);
});

this.unsubscribeNavigationBlur = this.props.navigation.addListener('blur', () => KeyDownListener.removeKeyDownPressListner(this.focusComposerOnKeyPress));
this.unsubscribeNavigationFocus = this.props.navigation.addListener('focus', () => KeyDownListener.addKeyDownPressListner(this.focusComposerOnKeyPress));
KeyDownListener.addKeyDownPressListner(this.focusComposerOnKeyPress);

this.updateComment(this.comment);

// Shows Popover Menu on Workspace Chat at first sign-in
Expand Down Expand Up @@ -276,6 +289,10 @@ class ReportActionCompose extends React.Component {

componentWillUnmount() {
ReportActionComposeFocusManager.clear();

KeyDownListener.removeKeyDownPressListner(this.focusComposerOnKeyPress);
this.unsubscribeNavigationBlur();
this.unsubscribeNavigationFocus();
}

onSelectionChange(e) {
Expand Down Expand Up @@ -664,20 +681,56 @@ class ReportActionCompose extends React.Component {
}

/**
* Callback for the emoji picker to add whatever emoji is chosen into the main input
*
* @param {String} emoji
* Callback to add whatever text is chosen into the main input (used f.e as callback for the emoji picker)
* @param {String} text
* @param {Boolean} shouldAddTrailSpace
*/
addEmojiToTextBox(emoji) {
this.updateComment(ComposerUtils.insertText(this.comment, this.state.selection, `${emoji} `));
replaceSelectionWithText(text, shouldAddTrailSpace = true) {
const updatedText = shouldAddTrailSpace ? `${text} ` : text;
const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0;
this.updateComment(ComposerUtils.insertText(this.comment, this.state.selection, updatedText));
this.setState((prevState) => ({
selection: {
start: prevState.selection.start + emoji.length + CONST.SPACE_LENGTH,
end: prevState.selection.start + emoji.length + CONST.SPACE_LENGTH,
start: prevState.selection.start + text.length + selectionSpaceLength,
end: prevState.selection.start + text.length + selectionSpaceLength,
},
}));
}

/**
* Check if the composer is visible. Returns true if the composer is not covered up by emoji picker or menu. False otherwise.
* @returns {Boolean}
*/
checkComposerVisibility() {
const isComposerCoveredUp = EmojiPickerActions.isEmojiPickerVisible() || this.state.isMenuVisible || this.props.modal.isVisible;
return !isComposerCoveredUp;
}

focusComposerOnKeyPress(e) {
const isComposerVisible = this.checkComposerVisibility();
if (!isComposerVisible) {
return;
}

// If the key pressed is non-character keys like Enter, Shift, ... do not focus
if (e.key.length > 1) {
return;
}
Comment on lines +715 to +718
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We forgot to consider the space as the key which also shouldn't focus the composer. This lead to issue #23508


// If a key is pressed in combination with Meta, Control or Alt do not focus
if (e.metaKey || e.ctrlKey || e.altKey) {
return;
}

// if we're typing on another input/text area, do not focus
if (['INPUT', 'TEXTAREA'].includes(e.target.nodeName)) {
return;
}

this.focus();
this.replaceSelectionWithText(e.key, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call was redundant. Later it caused #33710.
More details about the root cause: #33710 (comment)

}

/**
* Focus the composer text input
* @param {Boolean} [shouldelay=false] Impose delay before focusing the composer
Expand Down Expand Up @@ -1084,6 +1137,7 @@ class ReportActionCompose extends React.Component {
disabled={this.props.disabled}
>
<Composer
checkComposerVisibility={() => this.checkComposerVisibility()}
autoFocus={this.shouldAutoFocus}
multiline
ref={this.setTextInputRef}
Expand Down Expand Up @@ -1136,7 +1190,7 @@ class ReportActionCompose extends React.Component {
onModalHide={() => {
this.focus(true);
}}
onEmojiSelected={this.addEmojiToTextBox}
onEmojiSelected={this.replaceSelectionWithText}
/>
)}
<View
Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/sidebar/SidebarScreen/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useRef, useCallback} from 'react';
import React, {useCallback, useRef} from 'react';
import {InteractionManager} from 'react-native';
import {useFocusEffect} from '@react-navigation/native';
import sidebarPropTypes from './sidebarPropTypes';
Expand Down
Loading