diff --git a/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch
new file mode 100644
index 00000000000..84a233894f9
--- /dev/null
+++ b/patches/react-native+0.72.3+004+ModalKeyboardFlashing.patch
@@ -0,0 +1,18 @@
+diff --git a/node_modules/react-native/React/Views/RCTModalHostViewManager.m b/node_modules/react-native/React/Views/RCTModalHostViewManager.m
+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,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];
++ }
+ [[modalHostView reactViewController] presentViewController:viewController
+ animated:animated
+ completion:completionBlock];
diff --git a/src/components/AttachmentPicker/index.js b/src/components/AttachmentPicker/index.js
index e7653df2b4d..9ea94ae53d4 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 8ba7ae33606..8b1bb54da92 100644
--- a/src/components/AttachmentPicker/index.native.js
+++ b/src/components/AttachmentPicker/index.native.js
@@ -95,6 +95,7 @@ function AttachmentPicker({type, children}) {
const completeAttachmentSelection = useRef();
const onModalHide = useRef();
+ const onCanceled = useRef();
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -216,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);
};
@@ -239,6 +242,7 @@ function AttachmentPicker({type, children}) {
const pickAttachment = useCallback(
(attachments = []) => {
if (attachments.length === 0) {
+ onCanceled.current();
return Promise.resolve();
}
@@ -308,13 +312,16 @@ function AttachmentPicker({type, children}) {
*/
const renderChildren = () =>
children({
- openPicker: ({onPicked}) => open(onPicked),
+ openPicker: ({onPicked, onCanceled: newOnCanceled}) => open(onPicked, newOnCanceled),
});
return (
<>
{
+ close();
+ onCanceled.current();
+ }}
isVisible={isVisible}
anchorPosition={styles.createMenuPosition}
onModalHide={onModalHide.current}
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index 80ba1fc6034..79dd98d0e87 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 09df74329b2..b5f11a02650 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 00000000000..569e165da96
--- /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/libs/focusWithDelay.js b/src/libs/focusWithDelay.js
new file mode 100644
index 00000000000..367cc2b92f9
--- /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 143d5dd1243..00000000000
--- 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 faeb43147c5..00000000000
--- 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 27fb19fe157..00000000000
--- 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 2363c8d5742..5b027abced4 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -372,6 +372,17 @@ function ReportActionCompose({
focusWithDelay(textInputRef.current)(shouldDelay);
}, []);
+ const isNextModalWillOpenRef = useRef(false);
+ const isKeyboardVisibleWhenShowingModalRef = useRef(false);
+
+ const restoreKeyboardState = useCallback(() => {
+ if (!isKeyboardVisibleWhenShowingModalRef.current) {
+ return;
+ }
+ focus(true);
+ isKeyboardVisibleWhenShowingModalRef.current = false;
+ }, [focus]);
+
/**
* Update the value of the comment in Onyx
*
@@ -943,7 +954,8 @@ function ReportActionCompose({
shouldBlockEmojiCalc.current = false;
shouldBlockMentionCalc.current = false;
setIsAttachmentPreviewActive(false);
- }, []);
+ restoreKeyboardState();
+ }, [restoreKeyboardState]);
useEffect(() => {
const unsubscribeNavigationBlur = navigation.addListener('blur', () => KeyDownListener.removeKeyDownPressListner(focusComposerOnKeyPress));
@@ -982,10 +994,13 @@ function ReportActionCompose({
const prevIsModalVisible = usePrevious(modal.isVisible);
const prevIsFocused = usePrevious(isFocusedProp);
useEffect(() => {
+ if (modal.isVisible && !prevIsModalVisible) {
+ 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 && !modal.isVisible && isFocusedProp && (prevIsModalVisible || !prevIsFocused))) {
+ if (!(willBlurTextInputOnTapOutside && !isNextModalWillOpenRef.current && !modal.isVisible && isFocusedProp && (prevIsModalVisible || !prevIsFocused))) {
return;
}
@@ -1063,8 +1078,10 @@ function ReportActionCompose({
shouldBlockEmojiCalc.current = true;
shouldBlockMentionCalc.current = true;
}
+ isNextModalWillOpenRef.current = true;
openPicker({
onPicked: displayFileInModal,
+ onCanceled: restoreKeyboardState,
});
};
const menuItems = [
@@ -1133,6 +1150,10 @@ function ReportActionCompose({
ref={actionButtonRef}
onPress={(e) => {
e.preventDefault();
+ if (!willBlurTextInputOnTapOutside) {
+ isKeyboardVisibleWhenShowingModalRef.current = textInputRef.current.isFocused();
+ }
+ textInputRef.current.blur();
// Drop focus to avoid blue focus ring.
actionButtonRef.current.blur();
@@ -1150,7 +1171,10 @@ function ReportActionCompose({
setMenuVisibility(false)}
+ onClose={() => {
+ setMenuVisibility(false);
+ restoreKeyboardState();
+ }}
onItemSelected={(item, index) => {
setMenuVisibility(false);
@@ -1185,9 +1209,12 @@ function ReportActionCompose({
style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.flex4]}
maxLines={maxComposerLines}
onFocus={() => setIsFocused(true)}
- onBlur={() => {
+ onBlur={(e) => {
setIsFocused(false);
resetSuggestions();
+ if (e.relatedTarget && e.relatedTarget === actionButtonRef.current) {
+ isKeyboardVisibleWhenShowingModalRef.current = true;
+ }
}}
onClick={() => {
shouldBlockEmojiCalc.current = false;