From 11717c5772c631af6629d443270b8fbf107bf875 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 1 Aug 2023 20:41:32 +0800 Subject: [PATCH 01/12] fix keyboard flashing while clicking "Add attachment" --- patches/react-native+0.71.2-alpha.3.patch | 17 +++++++++- src/components/AttachmentPicker/index.js | 13 ++++++-- .../AttachmentPicker/index.native.js | 12 +++++-- src/components/Modal/BaseModal.js | 8 +++++ src/components/Modal/index.android.js | 10 ++++++ src/libs/ComposerFocusManager.js | 23 +++++++++++++ src/pages/home/report/ReportActionCompose.js | 32 ++++++++++++++----- 7 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/libs/ComposerFocusManager.js diff --git a/patches/react-native+0.71.2-alpha.3.patch b/patches/react-native+0.71.2-alpha.3.patch index 822ca9daec9c..e48c71bb2151 100644 --- a/patches/react-native+0.71.2-alpha.3.patch +++ b/patches/react-native+0.71.2-alpha.3.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js -index 2f48f9e..ac7a416 100644 +index 2f48f9e..e26d677 100644 --- a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -65,6 +65,7 @@ class KeyboardAvoidingView extends React.Component { @@ -86,3 +86,18 @@ index 2f48f9e..ac7a416 100644 componentDidMount(): void { if (Platform.OS === 'ios') { this._subscriptions = [ +diff --git a/node_modules/react-native/React/Views/RCTModalHostViewManager.m b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +index 4b9f9ad..4992874 100644 +--- a/node_modules/react-native/React/Views/RCTModalHostViewManager.m ++++ b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +@@ -79,6 +79,10 @@ RCT_EXPORT_MODULE() + if (self->_presentationBlock) { + self->_presentationBlock([modalHostView reactViewController], viewController, animated, completionBlock); + } else { ++ UIWindow *window = RCTKeyWindow(); ++ if (window && window.rootViewController && [window.rootViewController.view isFirstResponder]) { ++ [window.rootViewController.view resignFirstResponder]; ++ } + [[modalHostView reactViewController] presentViewController:viewController + animated:animated + completion:completionBlock]; diff --git a/src/components/AttachmentPicker/index.js b/src/components/AttachmentPicker/index.js index e7653df2b4d0..d18165ab57ab 100644 --- a/src/components/AttachmentPicker/index.js +++ b/src/components/AttachmentPicker/index.js @@ -27,6 +27,8 @@ function getAcceptableFileTypes(type) { function AttachmentPicker(props) { const fileInput = useRef(); const onPicked = useRef(); + const onCanceled = useRef(() => {}); + return ( <> e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + if (!fileInput.current) { + return; + } + fileInput.current.addEventListener('cancel', () => onCanceled.current(), { once: true}); + }} accept={getAcceptableFileTypes(props.type)} /> {props.children({ - openPicker: ({onPicked: newOnPicked}) => { + openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => { onPicked.current = newOnPicked; fileInput.current.click(); + onCanceled.current = newOnCanceled; }, })} diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index b4b7d0b04c4e..a8f3b8d35c16 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -126,6 +126,7 @@ class AttachmentPicker extends Component { }); } + this.cancel = () => {}; this.close = this.close.bind(this); this.pickAttachment = this.pickAttachment.bind(this); this.removeKeyboardListener = this.removeKeyboardListener.bind(this); @@ -181,6 +182,7 @@ class AttachmentPicker extends Component { */ pickAttachment(attachments = []) { if (attachments.length === 0) { + this.cancel(); return; } @@ -342,7 +344,10 @@ class AttachmentPicker extends Component { */ renderChildren() { return this.props.children({ - openPicker: ({onPicked}) => this.open(onPicked), + openPicker: ({onPicked, onCanceled = () => {}}) => { + this.open(onPicked); + this.cancel = onCanceled; + }, }); } @@ -350,7 +355,10 @@ class AttachmentPicker extends Component { return ( <> { + this.close(); + this.cancel(); + }} isVisible={this.state.isVisible} anchorPosition={styles.createMenuPosition} onModalHide={this.onModalHide} diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 6d5bd5390416..155e80acab84 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -10,6 +10,7 @@ import {propTypes as modalPropTypes, defaultProps as modalDefaultProps} from './ import * as Modal from '../../libs/actions/Modal'; import getModalStyles from '../../styles/getModalStyles'; import variables from '../../styles/variables'; +import ComposerFocusManager from '../../libs/ComposerFocusManager'; const propTypes = { ...modalPropTypes, @@ -73,6 +74,9 @@ class BaseModal extends PureComponent { this.props.onModalHide(); } Modal.onModalDidClose(); + if (!this.props.fullscreen) { + ComposerFocusManager.setReadyToFocus(); + } } render() { @@ -109,6 +113,9 @@ class BaseModal extends PureComponent { // Note: Escape key on web/desktop will trigger onBackButtonPress callback // eslint-disable-next-line react/jsx-props-no-multi-spaces onBackButtonPress={this.props.onClose} + onModalWillShow={() => { + ComposerFocusManager.resetReadyToFocus(); + }} onModalShow={() => { if (this.props.shouldSetModalVisibility) { Modal.setModalVisibility(true); @@ -117,6 +124,7 @@ class BaseModal extends PureComponent { }} propagateSwipe={this.props.propagateSwipe} onModalHide={this.hideModal} + onDismiss={() => ComposerFocusManager.setReadyToFocus()} onSwipeComplete={this.props.onClose} swipeDirection={swipeDirection} isVisible={this.props.isVisible} diff --git a/src/components/Modal/index.android.js b/src/components/Modal/index.android.js index 09df74329b20..b5f11a02650a 100644 --- a/src/components/Modal/index.android.js +++ b/src/components/Modal/index.android.js @@ -1,7 +1,17 @@ import React from 'react'; +import {AppState} from 'react-native'; import withWindowDimensions from '../withWindowDimensions'; import BaseModal from './BaseModal'; import {propTypes, defaultProps} from './modalPropTypes'; +import ComposerFocusManager from '../../libs/ComposerFocusManager'; + +AppState.addEventListener('focus', () => { + ComposerFocusManager.setReadyToFocus(); +}); + +AppState.addEventListener('blur', () => { + ComposerFocusManager.resetReadyToFocus(); +}); // Only want to use useNativeDriver on Android. It has strange flashes issue on IOS // https://github.com/react-native-modal/react-native-modal#the-modal-flashes-in-a-weird-way-when-animating diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js new file mode 100644 index 000000000000..1f5f9d2d6f97 --- /dev/null +++ b/src/libs/ComposerFocusManager.js @@ -0,0 +1,23 @@ +let isReadyToFocusPromise = Promise.resolve(); +let resolveIsReadyToFocus; + +function resetReadyToFocus() { + isReadyToFocusPromise = new Promise(resolve => { + resolveIsReadyToFocus = resolve; + }); +} +function setReadyToFocus() { + if (!resolveIsReadyToFocus) { + return; + } + resolveIsReadyToFocus() +} +function isReadyToFocus() { + return isReadyToFocusPromise; +} + +export default { + resetReadyToFocus, + setReadyToFocus, + isReadyToFocus, +}; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 644a90216091..6766cbb0e8b4 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -59,6 +59,7 @@ import * as KeyDownListener from '../../../libs/KeyboardShortcut/KeyDownPressLis import * as EmojiPickerActions from '../../../libs/actions/EmojiPickerAction'; import withAnimatedRef from '../../../components/withAnimatedRef'; import updatePropsPaperWorklet from '../../../libs/updatePropsPaperWorklet'; +import ComposerFocusManager from '../../../libs/ComposerFocusManager'; const propTypes = { /** Beta features list */ @@ -221,6 +222,8 @@ class ReportActionCompose extends React.Component { this.unsubscribeNavigationBlur = () => null; this.unsubscribeNavigationFocus = () => null; + + this.shouldFocusAfterClosingModal = true; this.state = { isFocused: this.shouldFocusInputOnScreenFocus && !this.props.modal.isVisible && !this.props.modal.willAlertModalBecomeVisible && this.props.shouldShowComposeInput, @@ -268,10 +271,13 @@ class ReportActionCompose extends React.Component { } componentDidUpdate(prevProps) { + if (this.props.modal.isVisible && !prevProps.modal.isVisible) { + this.shouldFocusAfterClosingModal = true; + } // 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.shouldFocusAfterClosingModal && !this.props.modal.isVisible && this.props.isFocused && (prevProps.modal.isVisible || !prevProps.isFocused)) { this.focus(); } @@ -757,13 +763,14 @@ class ReportActionCompose extends React.Component { if (!shouldelay) { this.textInput.focus(); - } else { - // Keyboard is not opened after Emoji Picker is closed - // SetTimeout is used as a workaround - // https://github.com/react-native-modal/react-native-modal/issues/114 - // We carefully choose a delay. 100ms is found enough for keyboard to open. - setTimeout(() => this.textInput.focus(), 100); + return; } + ComposerFocusManager.isReadyToFocus().then(() => { + if (!this.textInput) { + return; + } + this.textInput.focus(); + }); }); } @@ -1036,6 +1043,7 @@ class ReportActionCompose extends React.Component { this.shouldBlockEmojiCalc = false; this.shouldBlockMentionCalc = false; this.setState({isAttachmentPreviewActive: false}); + this.focus(true); }} > {({displayFileInModal}) => ( @@ -1095,6 +1103,7 @@ class ReportActionCompose extends React.Component { e.preventDefault(); // Drop focus to avoid blue focus ring. + this.textInput.blur(); this.actionButton.blur(); this.setMenuVisibility(true); }} @@ -1110,7 +1119,10 @@ class ReportActionCompose extends React.Component { this.setMenuVisibility(false)} + onClose={() => { + this.setMenuVisibility(false); + this.focus(true); + }} onItemSelected={() => this.setMenuVisibility(false)} anchorPosition={styles.createMenuPositionReportActionCompose(this.props.windowHeight)} anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM}} @@ -1128,8 +1140,12 @@ class ReportActionCompose extends React.Component { this.shouldBlockMentionCalc = true; } + this.shouldFocusAfterClosingModal = false; openPicker({ onPicked: displayFileInModal, + onCanceled: () => { + this.focus(true); + }, }); }, }, From e0764460e2910a2cc247ac2568282c8d7d78f049 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 1 Aug 2023 22:08:01 +0800 Subject: [PATCH 02/12] fix lint style --- src/components/AttachmentPicker/index.js | 2 +- src/libs/ComposerFocusManager.js | 4 ++-- src/pages/home/report/ReportActionCompose.js | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/AttachmentPicker/index.js b/src/components/AttachmentPicker/index.js index d18165ab57ab..9ea94ae53d42 100644 --- a/src/components/AttachmentPicker/index.js +++ b/src/components/AttachmentPicker/index.js @@ -53,7 +53,7 @@ function AttachmentPicker(props) { if (!fileInput.current) { return; } - fileInput.current.addEventListener('cancel', () => onCanceled.current(), { once: true}); + fileInput.current.addEventListener('cancel', () => onCanceled.current(), {once: true}); }} accept={getAcceptableFileTypes(props.type)} /> diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index 1f5f9d2d6f97..569e165da962 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -2,7 +2,7 @@ let isReadyToFocusPromise = Promise.resolve(); let resolveIsReadyToFocus; function resetReadyToFocus() { - isReadyToFocusPromise = new Promise(resolve => { + isReadyToFocusPromise = new Promise((resolve) => { resolveIsReadyToFocus = resolve; }); } @@ -10,7 +10,7 @@ function setReadyToFocus() { if (!resolveIsReadyToFocus) { return; } - resolveIsReadyToFocus() + resolveIsReadyToFocus(); } function isReadyToFocus() { return isReadyToFocusPromise; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 6766cbb0e8b4..27257805244f 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -222,7 +222,7 @@ class ReportActionCompose extends React.Component { this.unsubscribeNavigationBlur = () => null; this.unsubscribeNavigationFocus = () => null; - + this.shouldFocusAfterClosingModal = true; this.state = { @@ -277,7 +277,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.shouldFocusAfterClosingModal && !this.props.modal.isVisible && this.props.isFocused && (prevProps.modal.isVisible || !prevProps.isFocused)) { + if ( + this.willBlurTextInputOnTapOutside && + this.shouldFocusAfterClosingModal && + !this.props.modal.isVisible && + this.props.isFocused && + (prevProps.modal.isVisible || !prevProps.isFocused) + ) { this.focus(); } From 7ac72b55865e8fd4ceb7147137c3dd5c5b07d1cc Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 3 Aug 2023 16:08:12 +0800 Subject: [PATCH 03/12] upgrade patch approach for modal --- ...-native+0.72.1+004+ModalKeyboardFlashing.patch | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 patches/react-native+0.72.1+004+ModalKeyboardFlashing.patch diff --git a/patches/react-native+0.72.1+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.1+004+ModalKeyboardFlashing.patch new file mode 100644 index 000000000000..4f239084b4ec --- /dev/null +++ b/patches/react-native+0.72.1+004+ModalKeyboardFlashing.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/react-native/React/Views/RCTModalHostViewManager.m b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +index 4b9f9ad..4992874 100644 +--- a/node_modules/react-native/React/Views/RCTModalHostViewManager.m ++++ b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +@@ -79,6 +79,10 @@ RCT_EXPORT_MODULE() + if (self->_presentationBlock) { + self->_presentationBlock([modalHostView reactViewController], viewController, animated, completionBlock); + } else { ++ UIWindow *window = RCTKeyWindow(); ++ if (window && window.rootViewController && [window.rootViewController.view isFirstResponder]) { ++ [window.rootViewController.view resignFirstResponder]; ++ } + [[modalHostView reactViewController] presentViewController:viewController + animated:animated + completion:completionBlock]; From 98a5c4a0a362e45a22b77424f8e2fd599db3cc0f Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 8 Aug 2023 02:49:20 +0800 Subject: [PATCH 04/12] rename patch filename --- ....patch => react-native+0.72.1+005+ModalKeyboardFlashing.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/{react-native+0.72.1+004+ModalKeyboardFlashing.patch => react-native+0.72.1+005+ModalKeyboardFlashing.patch} (100%) diff --git a/patches/react-native+0.72.1+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.1+005+ModalKeyboardFlashing.patch similarity index 100% rename from patches/react-native+0.72.1+004+ModalKeyboardFlashing.patch rename to patches/react-native+0.72.1+005+ModalKeyboardFlashing.patch From 4d0afeb33d67311589c50efc702f7c97b3bcff13 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Sat, 12 Aug 2023 16:43:17 +0800 Subject: [PATCH 05/12] rename patch filename --- ....patch => react-native+0.72.3+004+ModalKeyboardFlashing.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/{react-native+0.72.1+005+ModalKeyboardFlashing.patch => react-native+0.72.3+004+ModalKeyboardFlashing.patch} (100%) diff --git a/patches/react-native+0.72.1+005+ModalKeyboardFlashing.patch b/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch similarity index 100% rename from patches/react-native+0.72.1+005+ModalKeyboardFlashing.patch rename to patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch From 887427ae55c7328a49e58523519891123a78c217 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Wed, 16 Aug 2023 22:46:21 +0800 Subject: [PATCH 06/12] replace setTimeout approach --- src/libs/focusWithDelay.js | 35 +++++++++++++++++ src/libs/focusWithDelay/focusWithDelay.js | 40 -------------------- src/libs/focusWithDelay/index.js | 7 ---- src/libs/focusWithDelay/index.native.js | 6 --- src/pages/home/report/ReportActionCompose.js | 33 ++-------------- 5 files changed, 39 insertions(+), 82 deletions(-) create mode 100644 src/libs/focusWithDelay.js delete mode 100644 src/libs/focusWithDelay/focusWithDelay.js delete mode 100644 src/libs/focusWithDelay/index.js delete mode 100644 src/libs/focusWithDelay/index.native.js diff --git a/src/libs/focusWithDelay.js b/src/libs/focusWithDelay.js new file mode 100644 index 000000000000..367cc2b92f9f --- /dev/null +++ b/src/libs/focusWithDelay.js @@ -0,0 +1,35 @@ +import {InteractionManager} from 'react-native'; +import ComposerFocusManager from './ComposerFocusManager'; + +/** + * Create a function that focuses a text input. + * @param {Object} textInput the text input to focus + * @returns {Function} a function that focuses the text input with a configurable delay + */ +function focusWithDelay(textInput) { + /** + * Focus the text input + * @param {Boolean} [shouldDelay=false] Impose delay before focusing the text input + */ + return (shouldDelay = false) => { + // There could be other animations running while we trigger manual focus. + // This prevents focus from making those animations janky. + InteractionManager.runAfterInteractions(() => { + if (!textInput) { + return; + } + if (!shouldDelay) { + textInput.focus(); + return; + } + ComposerFocusManager.isReadyToFocus().then(() => { + if (!textInput) { + return; + } + textInput.focus(); + }); + }); + }; +} + +export default focusWithDelay; diff --git a/src/libs/focusWithDelay/focusWithDelay.js b/src/libs/focusWithDelay/focusWithDelay.js deleted file mode 100644 index 143d5dd12430..000000000000 --- a/src/libs/focusWithDelay/focusWithDelay.js +++ /dev/null @@ -1,40 +0,0 @@ -import {InteractionManager} from 'react-native'; - -/** - * Creates a function that can be used to focus a text input - * @param {Boolean} disableDelay whether to force focus without a delay (on web and desktop) - * @returns {Function} a focusWithDelay function - */ -function focusWithDelay(disableDelay = false) { - /** - * Create a function that focuses a text input. - * @param {Object} textInput the text input to focus - * @returns {Function} a function that focuses the text input with a configurable delay - */ - return (textInput) => - /** - * Focus the text input - * @param {Boolean} [shouldDelay=false] Impose delay before focusing the text input - */ - (shouldDelay = false) => { - // There could be other animations running while we trigger manual focus. - // This prevents focus from making those animations janky. - InteractionManager.runAfterInteractions(() => { - if (!textInput) { - return; - } - - if (disableDelay || !shouldDelay) { - textInput.focus(); - } else { - // Keyboard is not opened after Emoji Picker is closed - // SetTimeout is used as a workaround - // https://github.com/react-native-modal/react-native-modal/issues/114 - // We carefully choose a delay. 100ms is found enough for keyboard to open. - setTimeout(() => textInput.focus(), 100); - } - }); - }; -} - -export default focusWithDelay; diff --git a/src/libs/focusWithDelay/index.js b/src/libs/focusWithDelay/index.js deleted file mode 100644 index faeb43147c5c..000000000000 --- a/src/libs/focusWithDelay/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import focusWithDelay from './focusWithDelay'; - -/** - * We pass true to disable the delay on the web because it doesn't require - * using the workaround (explained in the focusWithDelay.js file). - */ -export default focusWithDelay(true); diff --git a/src/libs/focusWithDelay/index.native.js b/src/libs/focusWithDelay/index.native.js deleted file mode 100644 index 27fb19fe1570..000000000000 --- a/src/libs/focusWithDelay/index.native.js +++ /dev/null @@ -1,6 +0,0 @@ -import focusWithDelay from './focusWithDelay'; - -/** - * We enable the delay on native to display the keyboard correctly - */ -export default focusWithDelay(false); diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index bbe17caf1e35..d105bdef731b 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {View, InteractionManager, LayoutAnimation, NativeModules, findNodeHandle} from 'react-native'; +import {View, LayoutAnimation, NativeModules, findNodeHandle} from 'react-native'; import {runOnJS} from 'react-native-reanimated'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; +import focusWithDelay from '../../../libs/focusWithDelay'; import styles from '../../../styles/styles'; import themeColors from '../../../styles/themes/default'; import Composer from '../../../components/Composer'; @@ -59,7 +60,6 @@ import * as KeyDownListener from '../../../libs/KeyboardShortcut/KeyDownPressLis import * as EmojiPickerActions from '../../../libs/actions/EmojiPickerAction'; import withAnimatedRef from '../../../components/withAnimatedRef'; import updatePropsPaperWorklet from '../../../libs/updatePropsPaperWorklet'; -import ComposerFocusManager from '../../../libs/ComposerFocusManager'; const propTypes = { /** Beta features list */ @@ -178,7 +178,7 @@ class ReportActionCompose extends React.Component { this.submitForm = this.submitForm.bind(this); this.setIsFocused = this.setIsFocused.bind(this); this.setIsFullComposerAvailable = this.setIsFullComposerAvailable.bind(this); - this.focus = this.focus.bind(this); + this.focus = focusWithDelay(this.textInput).bind(this); this.replaceSelectionWithText = this.replaceSelectionWithText.bind(this); this.focusComposerOnKeyPress = this.focusComposerOnKeyPress.bind(this); this.checkComposerVisibility = this.checkComposerVisibility.bind(this); @@ -393,6 +393,7 @@ class ReportActionCompose extends React.Component { if (_.isFunction(this.props.animatedRef)) { this.props.animatedRef(el); } + this.focus = focusWithDelay(this.textInput).bind(this); } /** @@ -753,32 +754,6 @@ class ReportActionCompose extends React.Component { this.replaceSelectionWithText(e.key, false); } - /** - * Focus the composer text input - * @param {Boolean} [shouldelay=false] Impose delay before focusing the composer - * @memberof ReportActionCompose - */ - focus(shouldelay = false) { - // There could be other animations running while we trigger manual focus. - // This prevents focus from making those animations janky. - InteractionManager.runAfterInteractions(() => { - if (!this.textInput) { - return; - } - - if (!shouldelay) { - this.textInput.focus(); - return; - } - ComposerFocusManager.isReadyToFocus().then(() => { - if (!this.textInput) { - return; - } - this.textInput.focus(); - }); - }); - } - /** * Save our report comment in Onyx. We debounce this method in the constructor so that it's not called too often * to update Onyx and re-render this component. From a021e0f92a780d4e3914f42f008ba0365ceb5ec3 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 17 Aug 2023 11:50:26 +0800 Subject: [PATCH 07/12] preserve keyboard state --- src/pages/home/report/ReportActionCompose.js | 42 ++++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index d105bdef731b..7ead07cbdb49 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -196,6 +196,7 @@ class ReportActionCompose extends React.Component { this.updateNumberOfLines = this.updateNumberOfLines.bind(this); this.showPopoverMenu = this.showPopoverMenu.bind(this); this.debouncedUpdateFrequentlyUsedEmojis = _.debounce(this.debouncedUpdateFrequentlyUsedEmojis.bind(this), 1000, false); + this.restoreKeyboardState = this.restoreKeyboardState.bind(this); this.comment = props.comment; this.insertedEmojis = []; @@ -215,6 +216,9 @@ class ReportActionCompose extends React.Component { this.shouldBlockEmojiCalc = false; this.shouldBlockMentionCalc = false; + this.willOpenNextModal = false; + this.isKeyboardVisibleWhenShowingModal = false; + // For mobile Safari, updating the selection prop on an unfocused input will cause it to automatically gain focus // and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style), // so we need to ensure that it is only updated after focus. @@ -223,8 +227,6 @@ class ReportActionCompose extends React.Component { this.unsubscribeNavigationBlur = () => null; this.unsubscribeNavigationFocus = () => null; - this.shouldFocusAfterClosingModal = true; - this.state = { isFocused: this.shouldFocusInputOnScreenFocus && !this.props.modal.isVisible && !this.props.modal.willAlertModalBecomeVisible && this.props.shouldShowComposeInput, isFullComposerAvailable: props.isComposerFullSize, @@ -274,18 +276,12 @@ class ReportActionCompose extends React.Component { componentDidUpdate(prevProps) { if (this.props.modal.isVisible && !prevProps.modal.isVisible) { - this.shouldFocusAfterClosingModal = true; + this.willOpenNextModal = false; } // 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.shouldFocusAfterClosingModal && - !this.props.modal.isVisible && - this.props.isFocused && - (prevProps.modal.isVisible || !prevProps.isFocused) - ) { + if (this.willBlurTextInputOnTapOutside && !this.willOpenNextModal && !this.props.modal.isVisible && this.props.isFocused && (prevProps.modal.isVisible || !prevProps.isFocused)) { this.focus(); } @@ -961,6 +957,14 @@ class ReportActionCompose extends React.Component { return true; } + restoreKeyboardState() { + if (!this.isKeyboardVisibleWhenShowingModal) { + return; + } + this.focus(true); + this.isKeyboardVisibleWhenShowingModal = false; + } + render() { const reportParticipants = _.without(lodashGet(this.props.report, 'participantAccountIDs', []), this.props.currentUserPersonalDetails.accountID); const participantsWithoutExpensifyAccountIDs = _.difference(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS); @@ -1023,7 +1027,7 @@ class ReportActionCompose extends React.Component { this.shouldBlockEmojiCalc = false; this.shouldBlockMentionCalc = false; this.setState({isAttachmentPreviewActive: false}); - this.focus(true); + this.restoreKeyboardState(); }} > {({displayFileInModal}) => ( @@ -1037,12 +1041,10 @@ class ReportActionCompose extends React.Component { this.shouldBlockEmojiCalc = true; this.shouldBlockMentionCalc = true; } - this.shouldFocusAfterClosingModal = false; + this.willOpenNextModal = true; openPicker({ onPicked: displayFileInModal, - onCanceled: () => { - this.focus(true); - }, + onCanceled: this.restoreKeyboardState, }); }; const menuItems = [ @@ -1111,6 +1113,9 @@ class ReportActionCompose extends React.Component { ref={this.actionButtonRef} onPress={(e) => { e.preventDefault(); + if (!this.willBlurTextInputOnTapOutside) { + this.isKeyboardVisibleWhenShowingModal = this.textInput.isFocused(); + } this.textInput.blur(); // Drop focus to avoid blue focus ring. @@ -1131,7 +1136,7 @@ class ReportActionCompose extends React.Component { isVisible={this.state.isMenuVisible} onClose={() => { this.setMenuVisibility(false); - this.focus(true); + this.restoreKeyboardState(); }} onItemSelected={(item, index) => { this.setMenuVisibility(false); @@ -1167,9 +1172,12 @@ class ReportActionCompose extends React.Component { style={[styles.textInputCompose, this.props.isComposerFullSize ? styles.textInputFullCompose : styles.flex4]} maxLines={maxComposerLines} onFocus={() => this.setIsFocused(true)} - onBlur={() => { + onBlur={(e) => { this.setIsFocused(false); this.resetSuggestions(); + if (e.relatedTarget && e.relatedTarget === this.actionButtonRef.current) { + this.isKeyboardVisibleWhenShowingModal = true; + } }} onClick={() => { this.shouldBlockEmojiCalc = false; From 3dfcf11a31d10ea5a960d193fd2737b94d00fd6a Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Sat, 19 Aug 2023 11:05:30 +0800 Subject: [PATCH 08/12] fix lint error --- src/pages/home/report/ReportActionCompose.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 8b3e6589ccfe..3f0c06172f23 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -362,7 +362,7 @@ function ReportActionCompose({ return translate('reportActionCompose.writeSomething'); }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); - + /** * Focus the composer text input * @param {Boolean} [shouldDelay=false] Impose delay before focusing the composer @@ -371,10 +371,10 @@ function ReportActionCompose({ const focus = useCallback((shouldDelay) => { focusWithDelay(textInputRef.current)(shouldDelay); }, []); - + const willOpenNextModal = useRef(false); const isKeyboardVisibleWhenShowingModal = useRef(false); - + const restoreKeyboardState = useCallback(() => { if (!isKeyboardVisibleWhenShowingModal.current) { return; @@ -1150,10 +1150,10 @@ function ReportActionCompose({ ref={actionButtonRef} onPress={(e) => { e.preventDefault(); - if (!willBlurTextInputOnTapOutside) { - isKeyboardVisibleWhenShowingModal.current = textInputRef.current.isFocused(); - } - textInputRef.current.blur(); + if (!willBlurTextInputOnTapOutside) { + isKeyboardVisibleWhenShowingModal.current = textInputRef.current.isFocused(); + } + textInputRef.current.blur(); // Drop focus to avoid blue focus ring. actionButtonRef.current.blur(); @@ -1222,7 +1222,7 @@ function ReportActionCompose({ }} onPasteFile={displayFileInModal} shouldClear={textInputShouldClear} - onClear={() => setTextInputShouldClear(false)} + onClear={() => setTextInputShouldClear(false)} isDisabled={isBlockedFromConcierge || disabled} selection={selection} onSelectionChange={onSelectionChange} From 4d165ce724d88c63b325bf8f6944a60453288a50 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 22 Aug 2023 19:43:46 +0800 Subject: [PATCH 09/12] add explanation for the patch file --- .../react-native+0.72.3+004+ModalKeyboardFlashing.patch | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch index 4f239084b4ec..84a233894f94 100644 --- a/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch +++ b/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch @@ -1,11 +1,14 @@ diff --git a/node_modules/react-native/React/Views/RCTModalHostViewManager.m b/node_modules/react-native/React/Views/RCTModalHostViewManager.m -index 4b9f9ad..4992874 100644 +index 4b9f9ad..b72984c 100644 --- a/node_modules/react-native/React/Views/RCTModalHostViewManager.m +++ b/node_modules/react-native/React/Views/RCTModalHostViewManager.m -@@ -79,6 +79,10 @@ RCT_EXPORT_MODULE() +@@ -79,6 +79,13 @@ RCT_EXPORT_MODULE() if (self->_presentationBlock) { self->_presentationBlock([modalHostView reactViewController], viewController, animated, completionBlock); } else { ++ // In our App, If an input is blurred and a modal is opened, the rootView will become the firstResponder, which ++ // will cause system to retain a wrong keyboard state, and then the keyboard to flicker when the modal is closed. ++ // We first resign the rootView to avoid this problem. + UIWindow *window = RCTKeyWindow(); + if (window && window.rootViewController && [window.rootViewController.view isFirstResponder]) { + [window.rootViewController.view resignFirstResponder]; From 09baf7ba7eda194a97e6604fda72e8bee80aacc7 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 22 Aug 2023 20:10:43 +0800 Subject: [PATCH 10/12] keep ref name consistent --- src/components/AttachmentPicker/index.native.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index 077ffcccc704..8b1bb54da920 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -95,7 +95,7 @@ function AttachmentPicker({type, children}) { const completeAttachmentSelection = useRef(); const onModalHide = useRef(); - const onCancel = useRef(); + const onCanceled = useRef(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -217,9 +217,11 @@ function AttachmentPicker({type, children}) { * Opens the attachment modal * * @param {function} onPickedHandler A callback that will be called with the selected attachment + * @param {function} onCanceledHandler A callback that will be called without a selected attachment */ - const open = (onPickedHandler) => { + const open = (onPickedHandler, onCanceledHandler = () => {}) => { completeAttachmentSelection.current = onPickedHandler; + onCanceled.current = onCanceledHandler; setIsVisible(true); }; @@ -240,7 +242,7 @@ function AttachmentPicker({type, children}) { const pickAttachment = useCallback( (attachments = []) => { if (attachments.length === 0) { - onCancel.current(); + onCanceled.current(); return Promise.resolve(); } @@ -310,10 +312,7 @@ function AttachmentPicker({type, children}) { */ const renderChildren = () => children({ - openPicker: ({onPicked, onCanceled = () => {}}) => { - open(onPicked); - onCancel.current = onCanceled; - }, + openPicker: ({onPicked, onCanceled: newOnCanceled}) => open(onPicked, newOnCanceled), }); return ( @@ -321,7 +320,7 @@ function AttachmentPicker({type, children}) { { close(); - onCancel.current(); + onCanceled.current(); }} isVisible={isVisible} anchorPosition={styles.createMenuPosition} From e272a1cc5fecb4fe6b5ef95c4dfa114af4e4562a Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 22 Aug 2023 20:41:30 +0800 Subject: [PATCH 11/12] rename variable name --- src/pages/home/report/ReportActionCompose.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 3f0c06172f23..d27100028033 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -372,7 +372,7 @@ function ReportActionCompose({ focusWithDelay(textInputRef.current)(shouldDelay); }, []); - const willOpenNextModal = useRef(false); + const isNextModalWillOpen = useRef(false); const isKeyboardVisibleWhenShowingModal = useRef(false); const restoreKeyboardState = useCallback(() => { @@ -995,12 +995,12 @@ function ReportActionCompose({ const prevIsFocused = usePrevious(isFocusedProp); useEffect(() => { if (modal.isVisible && !prevIsModalVisible) { - willOpenNextModal.current = false; + isNextModalWillOpen.current = false; } // 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 && !willOpenNextModal.current && !modal.isVisible && isFocusedProp && (prevIsModalVisible || !prevIsFocused))) { + if (!(willBlurTextInputOnTapOutside && !isNextModalWillOpen.current && !modal.isVisible && isFocusedProp && (prevIsModalVisible || !prevIsFocused))) { return; } @@ -1078,7 +1078,7 @@ function ReportActionCompose({ shouldBlockEmojiCalc.current = true; shouldBlockMentionCalc.current = true; } - willOpenNextModal.current = true; + isNextModalWillOpen.current = true; openPicker({ onPicked: displayFileInModal, onCanceled: restoreKeyboardState, From 67d3228561f50f0b0bac4731ecee1390c4febad9 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 22 Aug 2023 21:26:09 +0800 Subject: [PATCH 12/12] rename variable name --- src/pages/home/report/ReportActionCompose.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index d27100028033..5b027abced45 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -372,15 +372,15 @@ function ReportActionCompose({ focusWithDelay(textInputRef.current)(shouldDelay); }, []); - const isNextModalWillOpen = useRef(false); - const isKeyboardVisibleWhenShowingModal = useRef(false); + const isNextModalWillOpenRef = useRef(false); + const isKeyboardVisibleWhenShowingModalRef = useRef(false); const restoreKeyboardState = useCallback(() => { - if (!isKeyboardVisibleWhenShowingModal.current) { + if (!isKeyboardVisibleWhenShowingModalRef.current) { return; } focus(true); - isKeyboardVisibleWhenShowingModal.current = false; + isKeyboardVisibleWhenShowingModalRef.current = false; }, [focus]); /** @@ -995,12 +995,12 @@ function ReportActionCompose({ const prevIsFocused = usePrevious(isFocusedProp); useEffect(() => { if (modal.isVisible && !prevIsModalVisible) { - isNextModalWillOpen.current = false; + isNextModalWillOpenRef.current = false; } // 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 && !isNextModalWillOpen.current && !modal.isVisible && isFocusedProp && (prevIsModalVisible || !prevIsFocused))) { + if (!(willBlurTextInputOnTapOutside && !isNextModalWillOpenRef.current && !modal.isVisible && isFocusedProp && (prevIsModalVisible || !prevIsFocused))) { return; } @@ -1078,7 +1078,7 @@ function ReportActionCompose({ shouldBlockEmojiCalc.current = true; shouldBlockMentionCalc.current = true; } - isNextModalWillOpen.current = true; + isNextModalWillOpenRef.current = true; openPicker({ onPicked: displayFileInModal, onCanceled: restoreKeyboardState, @@ -1151,7 +1151,7 @@ function ReportActionCompose({ onPress={(e) => { e.preventDefault(); if (!willBlurTextInputOnTapOutside) { - isKeyboardVisibleWhenShowingModal.current = textInputRef.current.isFocused(); + isKeyboardVisibleWhenShowingModalRef.current = textInputRef.current.isFocused(); } textInputRef.current.blur(); @@ -1213,7 +1213,7 @@ function ReportActionCompose({ setIsFocused(false); resetSuggestions(); if (e.relatedTarget && e.relatedTarget === actionButtonRef.current) { - isKeyboardVisibleWhenShowingModal.current = true; + isKeyboardVisibleWhenShowingModalRef.current = true; } }} onClick={() => {