diff --git a/src/components/AttachmentPicker/attachmentPickerPropTypes.js b/src/components/AttachmentPicker/attachmentPickerPropTypes.js index 3b6fb7d041c5..1161819408ff 100644 --- a/src/components/AttachmentPicker/attachmentPickerPropTypes.js +++ b/src/components/AttachmentPicker/attachmentPickerPropTypes.js @@ -24,10 +24,14 @@ const propTypes = { /** The types of files that can be selected with this picker. */ type: PropTypes.oneOf([CONST.ATTACHMENT_PICKER_TYPE.FILE, CONST.ATTACHMENT_PICKER_TYPE.IMAGE]), + + /** Optional callback to fire when we want to do something once the modal is hidden. */ + onModalHide: PropTypes.func, }; const defaultProps = { type: CONST.ATTACHMENT_PICKER_TYPE.FILE, + onModalHide: () => {}, }; export {propTypes, defaultProps}; diff --git a/src/components/AttachmentPicker/index.js b/src/components/AttachmentPicker/index.js index fb2e95b88317..feb9bf714bb2 100644 --- a/src/components/AttachmentPicker/index.js +++ b/src/components/AttachmentPicker/index.js @@ -28,6 +28,7 @@ function getAcceptableFileTypes(type) { function AttachmentPicker(props) { const fileInput = useRef(); const onPicked = useRef(); + const onModalHide = useRef(); return ( <> e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + + // We add this focus event listener to call the onModalHide callback when user does not select any files + // i.e. clicks cancel in the native file picker modal - this is when the app gets the focus back + window.addEventListener('focus', onModalHide.current, { + once: true, // this removes the listener after running once + }); + }} accept={getAcceptableFileTypes(props.type)} /> {props.children({ - openPicker: ({onPicked: newOnPicked}) => { + openPicker: ({onPicked: newOnPicked, onModalHide: newOnModalHide}) => { onPicked.current = newOnPicked; + onModalHide.current = newOnModalHide; fileInput.current.click(); }, })} diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index b0b1222316f0..3e2cc268c527 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -100,6 +100,7 @@ class AttachmentPicker extends Component { this.state = { isVisible: false, + onModalHide: () => {}, focusedIndex: -1, }; @@ -232,6 +233,8 @@ class AttachmentPicker extends Component { return new Promise((resolve, reject) => { imagePickerFunc(getImagePickerOptions(this.props.type), (response) => { if (response.didCancel) { + this.state.onModalHide(); + // When the user cancelled resolve with no attachment return resolve(); } @@ -276,6 +279,7 @@ class AttachmentPicker extends Component { showDocumentPicker() { return RNDocumentPicker.pick(documentPickerOptions).catch((error) => { if (RNDocumentPicker.isCancel(error)) { + this.state.onModalHide(); return; } @@ -341,7 +345,12 @@ class AttachmentPicker extends Component { */ renderChildren() { return this.props.children({ - openPicker: ({onPicked}) => this.open(onPicked), + openPicker: ({onPicked, onModalHide}) => { + this.open(onPicked); + if (onModalHide) { + this.setState({onModalHide}); + } + }, }); } @@ -352,7 +361,15 @@ class AttachmentPicker extends Component { onClose={this.close} isVisible={this.state.isVisible} anchorPosition={styles.createMenuPosition} - onModalHide={this.onModalHide} + onModalHide={() => { + // this.modalHide is triggered when the modal is closed by selecting an item and + // this.state.onModalHide is triggered when the modal is closed by touching outside or back button + if (this.onModalHide) { + this.onModalHide(); + } else { + this.state.onModalHide(); + } + }} > { + if (!props.isVisible) { + setIsVisible(false); + } else if (props.isKeyboardShown) { + const keyboardListener = Keyboard.addListener('keyboardDidHide', () => { + setIsVisible(true); + keyboardListener.remove(); + }); + Keyboard.dismiss(); + } else { + setIsVisible(true); + } + }, [props.isVisible, props.isKeyboardShown]); + return ( {props.children} @@ -17,4 +40,4 @@ function Modal(props) { Modal.propTypes = propTypes; Modal.defaultProps = defaultProps; Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default compose(withWindowDimensions, withKeyboardState)(Modal); diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js index 1cafa9e12664..33ad89cd198e 100644 --- a/src/components/PopoverMenu/index.js +++ b/src/components/PopoverMenu/index.js @@ -61,19 +61,24 @@ function PopoverMenu(props) { {isActive: props.isVisible}, ); + const resetFocusAndHideModal = () => { + setFocusedIndex(-1); // Reset the focusedIndex on modal hide + if (selectedItemIndex !== null) { + props.menuItems[selectedItemIndex].onSelected(); + setSelectedItemIndex(null); + } else if (props.onModalHide) { + // trigger the onModalHide callback only when modal is closed by clicking outside or back button + props.onModalHide(); + } + }; + return ( { - setFocusedIndex(-1); - if (selectedItemIndex !== null) { - props.menuItems[selectedItemIndex].onSelected(); - setSelectedItemIndex(null); - } - }} + onModalHide={resetFocusAndHideModal} animationIn={props.animationIn} animationOut={props.animationOut} animationInTiming={props.animationInTiming} diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index e5a5bf61716f..3680106e7a79 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -177,6 +177,7 @@ class ReportActionCompose extends React.Component { this.getMoneyRequestOptions = this.getMoneyRequestOptions.bind(this); this.getTaskOption = this.getTaskOption.bind(this); this.addAttachment = this.addAttachment.bind(this); + this.finishAddAttachmentFlow = this.finishAddAttachmentFlow.bind(this); this.insertSelectedEmoji = this.insertSelectedEmoji.bind(this); this.insertSelectedMention = this.insertSelectedMention.bind(this); this.setExceededMaxCommentLength = this.setExceededMaxCommentLength.bind(this); @@ -206,6 +207,7 @@ class ReportActionCompose extends React.Component { const isMobileSafari = Browser.isMobileSafari(); this.state = { + isInAddAttachmentFlow: false, isFocused: this.shouldFocusInputOnScreenFocus && !this.props.modal.isVisible && !this.props.modal.willAlertModalBecomeVisible && this.props.shouldShowComposeInput, isFullComposerAvailable: props.isComposerFullSize, textInputShouldClear: false, @@ -257,7 +259,13 @@ class ReportActionCompose extends React.Component { // 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 (this.willBlurTextInputOnTapOutside && !this.props.modal.isVisible && this.props.isFocused && (prevProps.modal.isVisible || !prevProps.isFocused)) { + if ( + this.willBlurTextInputOnTapOutside && + !this.props.modal.isVisible && + this.props.isFocused && + !this.state.isInAddAttachmentFlow && + (prevProps.modal.isVisible || !prevProps.isFocused) + ) { this.focus(); } @@ -412,6 +420,15 @@ class ReportActionCompose extends React.Component { } } + /** + * Set isInAddAttachmentFlow state + * + * @param {Boolean} value + */ + setIsInAddAttachmentFlow(value) { + this.setState({isInAddAttachmentFlow: value}); + } + /** * Determines if we can show the task option * @param {Array} reportParticipants @@ -878,6 +895,17 @@ class ReportActionCompose extends React.Component { this.setTextInputShouldClear(false); } + /** + * Set the focus back to the composer + */ + finishAddAttachmentFlow() { + this.setIsInAddAttachmentFlow(false); + + // Explicitly focus the composer from here as componentDidUpdate does not do that in native + // because willBlurTextInputOnTapOutside is false on native which doesn't let it focus + this.focus(true); + } + /** * Add a new comment to this chat * @@ -953,6 +981,7 @@ class ReportActionCompose extends React.Component { onConfirm={this.addAttachment} onModalShow={() => this.setState({isAttachmentPreviewActive: true})} onModalHide={() => { + this.finishAddAttachmentFlow(); this.shouldBlockEmojiCalc = false; this.shouldBlockMentionCalc = false; this.setState({isAttachmentPreviewActive: false}); @@ -1016,6 +1045,10 @@ class ReportActionCompose extends React.Component { // Drop focus to avoid blue focus ring. this.actionButton.blur(); + + // we blur the input here to avoid an android specific issue where the keyboard + // doesn't open when we re-focus the input due to it never losing focus + this.textInput.blur(); this.setMenuVisibility(true); }} style={styles.composerSizeButton} @@ -1031,7 +1064,11 @@ class ReportActionCompose extends React.Component { animationInTiming={CONST.ANIMATION_IN_TIMING} isVisible={this.state.isMenuVisible} onClose={() => this.setMenuVisibility(false)} - onItemSelected={() => this.setMenuVisibility(false)} + onModalHide={this.finishAddAttachmentFlow} + onItemSelected={(item) => { + this.setMenuVisibility(false); + this.setIsInAddAttachmentFlow(item.text === this.props.translate('reportActionCompose.addAttachment')); + }} anchorPosition={styles.createMenuPositionReportActionCompose(this.props.windowHeight)} anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM}} menuItems={[ @@ -1050,6 +1087,7 @@ class ReportActionCompose extends React.Component { openPicker({ onPicked: displayFileInModal, + onModalHide: this.finishAddAttachmentFlow, }); }, },