From b6f237f1ad2bee2a4ab91182ac7920b8238abd2f Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 Dec 2023 10:28:30 +0100 Subject: [PATCH 01/10] Use debounce in Aztec's blur function --- packages/react-native-aztec/src/AztecInputState.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 5f0a1dd7596284..8ef33aea6747d3 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -3,6 +3,11 @@ */ import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; +/** + * WordPress dependencies + */ +import { debounce } from '@wordpress/compose'; + /** @typedef {import('@wordpress/element').RefObject} RefObject */ const focusChangeListeners = []; @@ -131,20 +136,25 @@ export const focusInput = ( element ) => { * @param {RefObject} element Element to be focused. */ export const focus = ( element ) => { + // If other blur events happen at the same time that focus is triggered, the focus event + // will take precedence and cancels pending blur events. + blur.cancel(); TextInputState.focusTextInput( element ); notifyInputChange(); }; /** * Unfocuses the specified element. + * This function uses debounce to avoid conflicts with the focus event when both are + * triggered at the same time. Focus events will take precedence. * * @param {RefObject} element Element to be unfocused. */ -export const blur = ( element ) => { +export const blur = debounce( ( element ) => { TextInputState.blurTextInput( element ); setCurrentCaretData( null ); notifyInputChange(); -}; +}, 0 ); /** * Unfocuses the current focused element. From a71b1c5df9e3cc9f3f9067cbb7e2d978f69b8919 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 Dec 2023 11:37:28 +0100 Subject: [PATCH 02/10] Execute `focus` UI block before other blocks --- .../ios/RNTAztecView/RCTAztecViewManager.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift index 184d465e5d25cd..8806c780779445 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift @@ -34,6 +34,17 @@ public class RCTAztecViewManager: RCTViewManager { return view } + /// This method is similar to `executeBlock` but prepends the block to execute it before other pending blocks. + func executeBlockBeforeOthers(viewTag: NSNumber, block: @escaping (RCTAztecView) -> Void) { + self.bridge.uiManager.prependUIBlock { (uiManager, viewRegistry) in + let view = viewRegistry?[viewTag] + guard let aztecView = view as? RCTAztecView else { + return + } + block(aztecView) + } + } + func executeBlock(viewTag: NSNumber, block: @escaping (RCTAztecView) -> Void) { self.bridge.uiManager.addUIBlock { (uiManager, viewRegistry) in let view = viewRegistry?[viewTag] @@ -69,7 +80,7 @@ public class RCTAztecViewManager: RCTViewManager { @objc func focus(_ viewTag: NSNumber) -> Void { - self.executeBlock(viewTag: viewTag) { (aztecView) in + self.executeBlockBeforeOthers(viewTag: viewTag) { (aztecView) in aztecView.reactFocus() } } From 63913e823a0abd0aca15f8e2a44cf06cb6f21e75 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 Dec 2023 19:12:51 +0100 Subject: [PATCH 03/10] Add `hideAndroidSoftKeyboard` to RN bridge --- .../RNReactNativeGutenbergBridgeModule.java | 16 ++++++++++++++++ packages/react-native-bridge/index.js | 15 +++++++++++++++ test/native/setup.js | 1 + 3 files changed, 32 insertions(+) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 0073db769d9cd5..643c67db64b8c4 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -1,11 +1,14 @@ package org.wordpress.mobile.ReactNativeGutenbergBridge; +import android.app.Activity; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; +import android.view.View; +import android.view.inputmethod.InputMethodManager; import androidx.annotation.Nullable; @@ -550,4 +553,17 @@ private ConnectionStatusCallback requestConnectionStatusCallback(final Callback } }; } + + @ReactMethod + public void hideAndroidSoftKeyboard() { + Activity currentActivity = mReactContext.getCurrentActivity(); + if (currentActivity != null) { + View currentFocusedView = currentActivity.getCurrentFocus(); + if (currentFocusedView != null) { + InputMethodManager imm = + (InputMethodManager) mReactContext.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(currentFocusedView.getWindowToken(), 0); + } + } + } } diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 8e9065cc568e56..1a9d5bde14596b 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -515,6 +515,21 @@ export function sendEventToHost( eventName, properties ) { ); } +/** + * Hides Android's soft keyboard. + * + * @return {void} + */ +export function hideAndroidSoftKeyboard() { + if ( isIOS ) { + /* eslint-disable-next-line no-console */ + console.warn( 'hideAndroidSoftKeyboard is not supported on iOS' ); + return; + } + + RNReactNativeGutenbergBridge.hideAndroidSoftKeyboard(); +} + /** * Generate haptic feedback. */ diff --git a/test/native/setup.js b/test/native/setup.js index 0f4c9f9eda20c9..fafd6fc414f3f3 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -109,6 +109,7 @@ jest.mock( '@wordpress/react-native-bridge', () => { subscribeOnRedoPressed: jest.fn(), useIsConnected: jest.fn( () => ( { isConnected: true } ) ), editorDidMount: jest.fn(), + hideAndroidSoftKeyboard: jest.fn(), editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), subscribeMediaSave: jest.fn(), From 634867f072bda9b6419c6c85162d5d2afdef386c Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 Dec 2023 19:14:21 +0100 Subject: [PATCH 04/10] Add `blurOnUnmount` to Aztec input state manager. This function will help us to deal with the special case of unfocusing an Aztec view upon unmounting. --- .../react-native-aztec/src/AztecInputState.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 8ef33aea6747d3..7a620b1a6b1df9 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -1,12 +1,14 @@ /** * External dependencies */ +import { Platform } from 'react-native'; import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; /** * WordPress dependencies */ import { debounce } from '@wordpress/compose'; +import { hideAndroidSoftKeyboard } from '@wordpress/react-native-bridge'; /** @typedef {import('@wordpress/element').RefObject} RefObject */ @@ -139,6 +141,9 @@ export const focus = ( element ) => { // If other blur events happen at the same time that focus is triggered, the focus event // will take precedence and cancels pending blur events. blur.cancel(); + // Similar to blur events, we also need to cancel potential keyboard dismiss. + dismissKeyboardDebounce.cancel(); + TextInputState.focusTextInput( element ); notifyInputChange(); }; @@ -156,6 +161,34 @@ export const blur = debounce( ( element ) => { notifyInputChange(); }, 0 ); +/** + * Unfocuses the specified element in case it's about to be unmounted. + * + * On iOS text inputs are automatically unfocused and keyboard dimissed when they + * are removed. However, this is not the case on Android, where text inputs are + * unfocused but the keyboard remains open. + * + * For dismissing the keyboard we use debounce to avoid conflicts with the focus + * event when both are triggered at the same time. + * + * Note that we can't trigger the blur event as it's likely that the Aztec view is no + * longer available when the event is executed and will produce an exception. + * + * @param {RefObject} element Element to be unfocused. + */ +export const blurOnUnmount = ( element ) => { + if ( getCurrentFocusedElement() === element ) { + // If a blur event was triggered before unmount, we need to cancel them to avoid + // exceptions. + blur.cancel(); + if ( Platform.OS === 'android' ) { + dismissKeyboardDebounce(); + } + } +}; + +const dismissKeyboardDebounce = debounce( () => hideAndroidSoftKeyboard(), 0 ); + /** * Unfocuses the current focused element. */ From d3fb48686426302c434d329676a1a1607e1fa528 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 Dec 2023 19:15:42 +0100 Subject: [PATCH 05/10] Dismiss keyboard when Aztec view unmounts This was previously handled in the `RichText` component. --- .../src/components/rich-text/native/index.native.js | 6 ------ packages/react-native-aztec/src/AztecView.js | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index 165316fdbde769..d36ff0ce4bf844 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -823,12 +823,6 @@ export class RichText extends Component { } } - componentWillUnmount() { - if ( this._editor.isFocused() ) { - this._editor.blur(); - } - } - componentDidUpdate( prevProps ) { const { style, tagName } = this.props; const { currentFontSize } = this.state; diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index c9d0d633f1c14f..650790658ba32a 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -66,6 +66,10 @@ class AztecView extends Component { this.focus = this.focus.bind( this ); } + componentWillUnmount() { + AztecInputState.blurOnUnmount( this.aztecViewRef.current ); + } + dispatch( command, params ) { params = params || []; UIManager.dispatchViewManagerCommand( From 35fa91f0078547ab674c8dba3d1c50207a13dff3 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 Dec 2023 19:16:42 +0100 Subject: [PATCH 06/10] Fix unit test related to `AztecInputState` after adding debounce to `blur` function --- packages/react-native-aztec/src/test/AztecInputState.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-native-aztec/src/test/AztecInputState.test.js b/packages/react-native-aztec/src/test/AztecInputState.test.js index 4e5a59c6caf23d..e95d25a695c964 100644 --- a/packages/react-native-aztec/src/test/AztecInputState.test.js +++ b/packages/react-native-aztec/src/test/AztecInputState.test.js @@ -32,6 +32,8 @@ const updateCurrentFocusedInput = ( value ) => { notifyInputChange(); }; +jest.useFakeTimers(); + describe( 'Aztec Input State', () => { it( 'listens to focus change event', () => { const listener = jest.fn(); @@ -96,6 +98,7 @@ describe( 'Aztec Input State', () => { it( 'unfocuses an element', () => { blur( ref ); + jest.runAllTimers(); expect( TextInputState.blurTextInput ).toHaveBeenCalledWith( ref ); } ); } ); From 1af3437ded895f38562080ab5c05d2a7ce43c58b Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 14 Dec 2023 19:27:11 +0100 Subject: [PATCH 07/10] Remove console warning from `hideAndroidSoftKeyboard` --- packages/react-native-bridge/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 1a9d5bde14596b..2d8975df8e4244 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -522,8 +522,6 @@ export function sendEventToHost( eventName, properties ) { */ export function hideAndroidSoftKeyboard() { if ( isIOS ) { - /* eslint-disable-next-line no-console */ - console.warn( 'hideAndroidSoftKeyboard is not supported on iOS' ); return; } From 17dcda5b0a6020d53cc207541c6bd60bfb06bd06 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 15 Dec 2023 13:05:05 +0100 Subject: [PATCH 08/10] Update inline comments of `blurOnUnmount` function --- packages/react-native-aztec/src/AztecInputState.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 7a620b1a6b1df9..ca752d36e3d048 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -164,14 +164,14 @@ export const blur = debounce( ( element ) => { /** * Unfocuses the specified element in case it's about to be unmounted. * - * On iOS text inputs are automatically unfocused and keyboard dimissed when they + * On iOS text inputs are automatically unfocused and keyboard dismissed when they * are removed. However, this is not the case on Android, where text inputs are * unfocused but the keyboard remains open. * - * For dismissing the keyboard we use debounce to avoid conflicts with the focus + * For dismissing the keyboard, we use debounce to avoid conflicts with the focus * event when both are triggered at the same time. * - * Note that we can't trigger the blur event as it's likely that the Aztec view is no + * Note that we can't trigger the blur event, as it's likely that the Aztec view is no * longer available when the event is executed and will produce an exception. * * @param {RefObject} element Element to be unfocused. From 71b3409a7741cab96426e43eafbe258c917814ae Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco Date: Mon, 18 Dec 2023 17:37:49 +0100 Subject: [PATCH 09/10] [Mobile] - Android - Bring the Keyboard back when closing modals (#57069) * React Native Bridge - Android - Introduces showAndroidSoftKeyboard to show the keyboard if there's a focused TextInput * Mobile - Bottom Sheet - Adds usage of showAndroidSoftKeyboard when closing the Modal so it shows the Keyboard on Android for focused TextInputs * React Native Bridge - Android - Introduces hideAndroidSoftKeyboard to hide the keyboard without triggering blur events * React Native Bridge - Remove console warnings for unsupported methods, as their names are self-explanatory. * Update showAndroidSoftKeyboard to take into account when the window focus changed, when we show the Modals these are shown on top of the editor activity. It also adds an option to delay this for full screen modals * Mobile - BottomSheet - Enable hardwareAccelerated and useNativeDriverForBackdrop props to improve performance on Android * Update snapshots * Removes hasWindowFocus condition as it is not being called hence not needed * Refactor showAndroidSoftKeyboard to split into several functions, it also removes the delay functionality as it is no longer needed. It fixes an issue where mKeyboardRunnable was not being set. It removes the delay logic from the Bottom Sheet component and the bridge. * Updates createShowKeyboardRunnable to get the activity within the runnable instead of getting it as an param * Remove unneeded check --- .../src/mobile/bottom-sheet/index.native.js | 16 ++++- .../test/__snapshots__/modal.native.js.snap | 2 + .../RNReactNativeGutenbergBridgeModule.java | 59 ++++++++++++++++++- packages/react-native-bridge/index.js | 14 +++++ test/native/setup.js | 1 + 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 11918782f4dfb4..820115b4ffea79 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -19,7 +19,10 @@ import SafeArea from 'react-native-safe-area'; /** * WordPress dependencies */ -import { subscribeAndroidModalClosed } from '@wordpress/react-native-bridge'; +import { + subscribeAndroidModalClosed, + showAndroidSoftKeyboard, +} from '@wordpress/react-native-bridge'; import { Component } from '@wordpress/element'; import { withPreferredColorScheme } from '@wordpress/compose'; @@ -215,6 +218,11 @@ class BottomSheet extends Component { if ( this.androidModalClosedSubscription ) { this.androidModalClosedSubscription.remove(); } + + if ( this.props.isVisible ) { + showAndroidSoftKeyboard(); + } + if ( this.safeAreaEventSubscription === null ) { return; } @@ -315,6 +323,9 @@ class BottomSheet extends Component { onDismiss() { const { onDismiss } = this.props; + // Restore Keyboard Visibility + showAndroidSoftKeyboard(); + if ( onDismiss ) { onDismiss(); } @@ -368,6 +379,7 @@ class BottomSheet extends Component { onHardwareButtonPress() { const { onClose } = this.props; const { handleHardwareButtonPress } = this.state; + if ( handleHardwareButtonPress && handleHardwareButtonPress() ) { return; } @@ -528,6 +540,8 @@ class BottomSheet extends Component { } onAccessibilityEscape={ this.onCloseBottomSheet } testID="bottom-sheet" + hardwareAccelerated={ true } + useNativeDriverForBackdrop={ true } { ...rest } > { subscribeOnRedoPressed: jest.fn(), useIsConnected: jest.fn( () => ( { isConnected: true } ) ), editorDidMount: jest.fn(), + showAndroidSoftKeyboard: jest.fn(), hideAndroidSoftKeyboard: jest.fn(), editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), From a2041a254f2aaca010b1a9a943a78fbe3ec1b7a0 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Mon, 18 Dec 2023 19:59:20 +0100 Subject: [PATCH 10/10] Update `react-native-editor` changelog --- packages/react-native-editor/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 6f4c1ee783e198..63cb545c7979cf 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -16,6 +16,7 @@ For each user feature we should also add a importance categorization label to i - [*] Guard against an Image block styles crash due to null block values [#56903] - [**] Fix crash when sharing unsupported media types on Android [#56791] - [**] Fix regressions with wrapper props and font size customization [#56985] +- [***] Avoid keyboard dismiss when interacting with text blocks [#57070] ## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686]