diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js index 26ab309d2e7a0..540bad7e8a518 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.native.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Platform, ScrollView, View } from 'react-native'; +import { Platform, ScrollView, View, Keyboard } from 'react-native'; /** * WordPress dependencies @@ -185,6 +185,12 @@ export default compose( [ onHideKeyboard() { clearSelectedBlock(); togglePostTitleSelection( false ); + // Explicitly dismiss the keyboard for circumstances where Aztec race + // conditions lead to one block's rich text focused, but a different + // non-rich-text block selected. In this context, merely clearing block + // selection does not trigger a blur event and therefore will not + // dismiss the keyboard. + Keyboard.dismiss(); }, }; } ), diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 973b8179ea5ec..94b612be50e7e 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -83,6 +83,15 @@ export const notifyInputChange = () => { } }; +/** + * Sets the current focused element ref held within TextInputState. + * + * @param {RefObject} element Element to be set as the focused element. + */ +export const focusInput = ( element ) => { + TextInputState.focusInput( element ); +}; + /** * Focuses the specified element. * diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index 456fde3ed8b69..1c50c2e5d434f 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -231,13 +231,41 @@ class AztecView extends Component { } } - _onAztecFocus( event ) { - // IMPORTANT: the onFocus events from Aztec are thrown away on Android as these are handled by onPress() in the upper level. - // It's necessary to do this otherwise onFocus may be set by `{...otherProps}` and thus the onPress + onFocus - // combination generate an infinite loop as described in https://github.com/wordpress-mobile/gutenberg-mobile/issues/302 - // For iOS, this is necessary to let the system know when Aztec was focused programatically. - if ( Platform.OS === 'ios' ) { - this._onPress( event ); + _onAztecFocus() { + // IMPORTANT: This function serves two purposes: + // + // Android: This intentional no-op function prevents focus loops originating + // when the native Aztec module programmatically focuses the instance. The + // no-op is explicitly passed as an `onFocus` prop to avoid future prop + // spreading from inadvertently introducing focus loops. The user-facing + // focus of the element is handled by `onPress` instead. + // + // See: https://github.com/wordpress-mobile/gutenberg-mobile/issues/302 + // + // iOS: Programmatic focus from the native Aztec module is required to + // ensure the React-based `TextStateInput` ref is properly set when focus + // is *returned* to an instance, e.g. dismissing a bottom sheet. If the ref + // is not updated, attempts to dismiss the keyboard via the `ToolbarButton` + // will fail. + // + // See: https://github.com/wordpress-mobile/gutenberg-mobile/issues/702 + if ( + // The Android keyboard is, likely erroneously, already dismissed in the + // contexts where programmatic focus may be required on iOS. + // + // - https://github.com/WordPress/gutenberg/issues/28748 + // - https://github.com/WordPress/gutenberg/issues/29048 + // - https://github.com/wordpress-mobile/WordPress-Android/issues/16167 + Platform.OS === 'ios' + ) { + // Programmatically swapping input focus creates an infinite loop if the + // user taps a different input in between the programmatic focus and + // the resulting update to the React Native TextInputState focused element + // ref. To mitigate this, the below updates the focused element ref, but + // does not call the native focus methods. + // + // See: https://github.com/wordpress-mobile/WordPress-iOS/issues/18783 + AztecInputState.focusInput( this.aztecViewRef.current ); } } @@ -269,9 +297,7 @@ class AztecView extends Component { onBackspace={ this.props.onKeyDown && this._onBackspace } onKeyDown={ this.props.onKeyDown && this._onKeyDown } deleteEnter={ this.props.deleteEnter } - // IMPORTANT: the onFocus events are thrown away as these are handled by onPress() in the upper level. - // It's necessary to do this otherwise onFocus may be set by `{...otherProps}` and thus the onPress + onFocus - // combination generate an infinite loop as described in https://github.com/wordpress-mobile/gutenberg-mobile/issues/302 + // IMPORTANT: Do not remove the `onFocus` prop, see `_onAztecFocus` onFocus={ this._onAztecFocus } onBlur={ this._onBlur } ref={ this.aztecViewRef } diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 2cd7647c0e0f6..f5e1e63e5731c 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [*] [iOS] Fix focus loop when quickly tapping the block appender [#44988] ## 1.85.0 - [*] [iOS] Fixed iOS Voice Control support within Image block captions. [#44850]