diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index da367ecc0ebea6..810e23e4c1442a 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -1,12 +1,12 @@ /** * External dependencies */ -import { View, Platform, TouchableWithoutFeedback } from 'react-native'; +import { View, Platform, Pressable } from 'react-native'; /** * WordPress dependencies */ -import { useRef, useState } from '@wordpress/element'; +import { useRef, useState, useCallback } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; import { @@ -372,20 +372,20 @@ function Footer( { renderFooterAppender, withFooter, } ) { + const onAddParagraphBlock = useCallback( () => { + const paragraphBlock = createBlock( 'core/paragraph' ); + addBlockToEndOfPost( paragraphBlock ); + }, [ addBlockToEndOfPost ] ); + if ( ! isReadOnly && withFooter ) { return ( - <> - { - const paragraphBlock = createBlock( 'core/paragraph' ); - addBlockToEndOfPost( paragraphBlock ); - } } - > - - - + + + ); } else if ( renderFooterAppender ) { return { renderFooterAppender() }; diff --git a/packages/block-editor/src/components/default-block-appender/index.native.js b/packages/block-editor/src/components/default-block-appender/index.native.js index dae0750b8e30d4..82ff8b7c8d4029 100644 --- a/packages/block-editor/src/components/default-block-appender/index.native.js +++ b/packages/block-editor/src/components/default-block-appender/index.native.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { TouchableWithoutFeedback, View } from 'react-native'; +import { Pressable, View } from 'react-native'; /** * WordPress dependencies @@ -20,6 +20,9 @@ import BlockInsertionPoint from '../block-list/insertion-point'; import styles from './style.scss'; import { store as blockEditorStore } from '../../store'; +const hitSlop = { top: 22, bottom: 22, left: 22, right: 22 }; +const noop = () => {}; + export function DefaultBlockAppender( { isLocked, isVisible, @@ -37,22 +40,21 @@ export function DefaultBlockAppender( { ? decodeEntities( placeholder ) : __( 'Start writing…' ); + const appenderStyles = [ + styles.blockHolder, + showSeparator && containerStyle, + ]; + return ( - - + + { showSeparator ? ( ) : ( - {} } /> + ) } - + ); } diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 8a1737118d1d13..5f0a1dd7596284 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -116,6 +116,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 4d90d13974c8ec..e05737f5fb0f99 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -237,14 +237,49 @@ 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' ) { + // 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' + ) { this.updateCaretData( event ); - this._onPress( event ); + if ( ! this.isFocused() ) { + // 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 ); + + // Calling _onFocus is needed to trigger provided onFocus callbacks + // which are needed to prevent undesired results like having a focused + // TextInput when another element has the focus. + this._onFocus( event ); + } } } @@ -285,9 +320,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 4ddfda16c8c78d..e757ad02bd62d5 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,9 @@ For each user feature we should also add a importance categorization label to i ## Unreleased +## 1.100.2 +- [**] Fix iOS Focus loop for RichText components [#53217] + ## 1.100.1 - [**] Add WP hook for registering non-core blocks [#52791]