From c7423ea9fca2844472b552fdb56bd633e7ca5307 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 13:03:52 +0200 Subject: [PATCH 01/14] Add input state functionality to Aztec --- .../react-native-aztec/src/AztecInputState.js | 69 +++++++++++++++++++ packages/react-native-aztec/src/AztecView.js | 23 +++++-- 2 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 packages/react-native-aztec/src/AztecInputState.js diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js new file mode 100644 index 0000000000000..28387c971b18a --- /dev/null +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; + +const focusListeners = []; +const blurListeners = []; + +let currentFocusedElement = null; + +export const addFocusListener = ( listener ) => { + focusListeners.push( listener ); +}; + +export const addBlurListener = ( listener ) => { + blurListeners.push( listener ); +}; + +export const removeFocusListener = ( listener ) => { + const itemIndex = focusListeners.indexOf( listener ); + if ( itemIndex !== -1 ) { + focusListeners.splice( itemIndex, 1 ); + } +}; + +export const removeBlurListener = ( listener ) => { + const itemIndex = blurListeners.indexOf( listener ); + if ( itemIndex !== -1 ) { + blurListeners.splice( itemIndex, 1 ); + } +}; + +export const isFocused = () => { + return currentFocusedElement !== null; +}; + +export const getCurrentFocusedElement = () => { + return currentFocusedElement; +}; + +export const notifyFocus = ( element ) => { + currentFocusedElement = element; + + focusListeners.forEach( ( listener ) => { + listener( element ); + } ); +}; + +export const notifyBlur = ( element ) => { + currentFocusedElement = null; + + blurListeners.forEach( ( listener ) => { + listener( element ); + } ); +}; + +export const focus = ( element ) => { + TextInputState.focusTextInput( element ); +}; + +export const blur = ( element ) => { + TextInputState.blurTextInput( element ); +}; + +export const blurCurrentFocusedElement = () => { + if ( isFocused() ) { + blur( getCurrentFocusedElement() ); + } +}; diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index 96af6bfa366b0..3ce618a2d407e 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -7,13 +7,18 @@ import { TouchableWithoutFeedback, Platform, } from 'react-native'; -import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; + /** * WordPress dependencies */ import { Component, createRef } from '@wordpress/element'; import { ENTER, BACKSPACE } from '@wordpress/keycodes'; +/** + * Internal dependencies + */ +import * as AztecInputState from './AztecInputState'; + const AztecManager = UIManager.getViewManagerConfig( 'RCTAztecView' ); class AztecView extends Component { @@ -117,6 +122,8 @@ class AztecView extends Component { } _onFocus( event ) { + AztecInputState.notifyFocus( this.aztecViewRef.current ); + if ( ! this.props.onFocus ) { return; } @@ -127,7 +134,9 @@ class AztecView extends Component { _onBlur( event ) { this.selectionEndCaretY = null; - TextInputState.blurTextInput( this.aztecViewRef.current ); + + AztecInputState.blur( this.aztecViewRef.current ); + AztecInputState.notifyBlur( this.aztecViewRef.current ); if ( ! this.props.onBlur ) { return; @@ -179,16 +188,16 @@ class AztecView extends Component { } blur() { - TextInputState.blurTextInput( this.aztecViewRef.current ); + AztecInputState.blur( this.aztecViewRef.current ); } focus() { - TextInputState.focusTextInput( this.aztecViewRef.current ); + AztecInputState.focus( this.aztecViewRef.current ); } isFocused() { - const focusedField = TextInputState.currentlyFocusedInput(); - return focusedField && focusedField === this.aztecViewRef.current; + const focusedElement = AztecInputState.getCurrentFocusedElement(); + return focusedElement && focusedElement === this.aztecViewRef.current; } _onPress( event ) { @@ -251,4 +260,6 @@ class AztecView extends Component { const RCTAztecView = requireNativeComponent( 'RCTAztecView', AztecView ); +AztecView.InputState = AztecInputState; + export default AztecView; From ec750ce04050fffff0302e05c43b69f526f54f46 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 13:16:05 +0200 Subject: [PATCH 02/14] Control drag enabling with Aztec input state --- .../block-draggable/index.native.js | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js index b50122764e54e..feaf8625111cd 100644 --- a/packages/block-editor/src/components/block-draggable/index.native.js +++ b/packages/block-editor/src/components/block-draggable/index.native.js @@ -12,16 +12,22 @@ import Animated, { ZoomInEasyDown, ZoomOutEasyDown, } from 'react-native-reanimated'; -import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; /** * WordPress dependencies */ import { Draggable, DraggableTrigger } from '@wordpress/components'; import { select, useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useRef, useState, Platform } from '@wordpress/element'; +import { + useCallback, + useEffect, + useRef, + useState, + Platform, +} from '@wordpress/element'; import { getBlockType } from '@wordpress/blocks'; import { generateHapticFeedback } from '@wordpress/react-native-bridge'; +import RCTAztecView from '@wordpress/react-native-aztec'; /** * Internal dependencies @@ -256,6 +262,9 @@ const BlockDraggableWrapper = ( { children } ) => { */ const BlockDraggable = ( { clientId, children, enabled = true } ) => { const wasBeingDragged = useRef( false ); + const [ isEditingText, setIsEditingText ] = useState( + RCTAztecView.InputState.isFocused() + ); const draggingAnimation = { opacity: useSharedValue( 1 ), @@ -275,27 +284,25 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { ); }; - const { isDraggable, isBeingDragged, canDragBlock } = useSelect( + const { isDraggable, isBeingDragged, isBlockSelected } = useSelect( ( _select ) => { const { getBlockRootClientId, getTemplateLock, isBlockBeingDragged, - hasSelectedBlock, + getSelectedBlockClientId, } = _select( blockEditorStore ); const rootClientId = getBlockRootClientId( clientId ); const templateLock = rootClientId ? getTemplateLock( rootClientId ) : null; - const isAnyTextInputFocused = - TextInputState.currentlyFocusedInput() !== null; + const selectedBlockClientId = getSelectedBlockClientId(); return { isBeingDragged: isBlockBeingDragged( clientId ), isDraggable: 'all' !== templateLock, - canDragBlock: hasSelectedBlock() - ? ! isAnyTextInputFocused - : true, + isBlockSelected: + selectedBlockClientId && selectedBlockClientId === clientId, }; }, [ clientId ] @@ -312,6 +319,31 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { wasBeingDragged.current = isBeingDragged; }, [ isBeingDragged ] ); + const onFocusAztec = useCallback( () => { + setIsEditingText( true ); + }, [] ); + + const onBlurAztec = useCallback( () => { + setIsEditingText( false ); + }, [] ); + + const registerAztecListeners = () => { + RCTAztecView.InputState.addFocusListener( onFocusAztec ); + RCTAztecView.InputState.addBlurListener( onBlurAztec ); + }; + + const unregisterAztecListeners = () => { + RCTAztecView.InputState.removeFocusListener( onFocusAztec ); + RCTAztecView.InputState.removeBlurListener( onBlurAztec ); + }; + + useEffect( () => { + registerAztecListeners(); + return () => { + unregisterAztecListeners(); + }; + }, [] ); + const animatedWrapperStyles = useAnimatedStyle( () => { return { opacity: draggingAnimation.opacity.value, @@ -322,6 +354,8 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { styles[ 'draggable-wrapper__container' ], ]; + const canDragBlock = enabled && ( ! isBlockSelected || ! isEditingText ); + if ( ! isDraggable ) { return children( { isDraggable: false } ); } From f6fc9614845a96cb26ba46155322a77b0c3e9e1b Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 13:17:55 +0200 Subject: [PATCH 03/14] Force disabling text editing when dragging --- .../src/components/block-draggable/index.native.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js index feaf8625111cd..14880d1fa5ea4 100644 --- a/packages/block-editor/src/components/block-draggable/index.native.js +++ b/packages/block-editor/src/components/block-draggable/index.native.js @@ -374,6 +374,10 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { : DEFAULT_LONG_PRESS_MIN_DURATION, android: DEFAULT_LONG_PRESS_MIN_DURATION, } ) } + onLongPress={ () => { + // Ensure that no text input is focused when starting the dragging gesture in order to prevent conflicts with text editing. + RCTAztecView.InputState.blurCurrentFocusedElement(); + } } > { children( { isDraggable: true } ) } From ed93ab74bdfe6120c3eb90059134eaec56ba51b4 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 17:03:18 +0200 Subject: [PATCH 04/14] Add documentation to AztecInputState --- .../react-native-aztec/src/AztecInputState.js | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 28387c971b18a..461c2290fc117 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -3,19 +3,36 @@ */ import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; +/** @typedef {import('@wordpress/element').RefObject} RefObject */ + const focusListeners = []; const blurListeners = []; let currentFocusedElement = null; +/** + * Adds a listener that will be called when any Aztec view is focused. + * + * @param {Function} listener + */ export const addFocusListener = ( listener ) => { focusListeners.push( listener ); }; +/** + * Adds a listener that will be called when any Aztec view is unfocused. + * + * @param {Function} listener + */ export const addBlurListener = ( listener ) => { blurListeners.push( listener ); }; +/** + * Removes a listener from the focus listeners list. + * + * @param {Function} listener + */ export const removeFocusListener = ( listener ) => { const itemIndex = focusListeners.indexOf( listener ); if ( itemIndex !== -1 ) { @@ -23,6 +40,11 @@ export const removeFocusListener = ( listener ) => { } }; +/** + * Removes a listener from the blur listeners list. + * + * @param {Function} listener + */ export const removeBlurListener = ( listener ) => { const itemIndex = blurListeners.indexOf( listener ); if ( itemIndex !== -1 ) { @@ -30,14 +52,29 @@ export const removeBlurListener = ( listener ) => { } }; +/** + * Determines if any Aztec view is focused. + * + * @return {boolean} True if focused. + */ export const isFocused = () => { return currentFocusedElement !== null; }; +/** + * Returns the current focused element. + * + * @return {RefObject} Ref of the current focused element or `null` otherwise. + */ export const getCurrentFocusedElement = () => { return currentFocusedElement; }; +/** + * Notifies that an Aztec view is being focused. + * + * @param {RefObject} element Aztec view being focused. + */ export const notifyFocus = ( element ) => { currentFocusedElement = element; @@ -46,6 +83,11 @@ export const notifyFocus = ( element ) => { } ); }; +/** + * Notifies that an Aztec view is being unfocused. + * + * @param {RefObject} element Aztec view being unfocused. + */ export const notifyBlur = ( element ) => { currentFocusedElement = null; @@ -54,14 +96,27 @@ export const notifyBlur = ( element ) => { } ); }; +/** + * Focuses the specified element. + * + * @param {RefObject} element Element to be focused. + */ export const focus = ( element ) => { TextInputState.focusTextInput( element ); }; +/** + * Unfocuses the specified element. + * + * @param {RefObject} element Element to be unfocused. + */ export const blur = ( element ) => { TextInputState.blurTextInput( element ); }; +/** + * Unfocuses the current focused element. + */ export const blurCurrentFocusedElement = () => { if ( isFocused() ) { blur( getCurrentFocusedElement() ); From 5b140865c9af5ae4d1e831a2c16bde11a03dda8e Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 17:05:05 +0200 Subject: [PATCH 05/14] Update changelog --- packages/react-native-aztec/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-aztec/CHANGELOG.md b/packages/react-native-aztec/CHANGELOG.md index 5973442f6f736..5646165151470 100644 --- a/packages/react-native-aztec/CHANGELOG.md +++ b/packages/react-native-aztec/CHANGELOG.md @@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Bump Aztec-Android version to v1.5.1 [#36861] +- [*] Add text input state to Aztec view [#40480] ## 1.50.0 - [*] Block split/merge fix for a (never shipped) regression (Android only) [#29683] From ac7ac2f20271a2ae708f25a32eccb35282ae038c Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 17:22:26 +0200 Subject: [PATCH 06/14] Add tests for Aztec input state --- .../src/test/AztecInputState.test.js | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/react-native-aztec/src/test/AztecInputState.test.js diff --git a/packages/react-native-aztec/src/test/AztecInputState.test.js b/packages/react-native-aztec/src/test/AztecInputState.test.js new file mode 100644 index 0000000000000..eaa9ef21f4957 --- /dev/null +++ b/packages/react-native-aztec/src/test/AztecInputState.test.js @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; + +/** + * Internal dependencies + */ +import { + addBlurListener, + addFocusListener, + getCurrentFocusedElement, + isFocused, + focus, + blur, + notifyBlur, + notifyFocus, + removeFocusListener, + removeBlurListener, +} from '../AztecInputState'; + +jest.mock( + 'react-native/Libraries/Components/TextInput/TextInputState', + () => ( { + focusTextInput: jest.fn(), + blurTextInput: jest.fn(), + } ) +); + +const ref = { current: null }; + +describe( 'Aztec Input State', () => { + it( 'listens to focus event', () => { + const listener = jest.fn(); + addFocusListener( listener ); + notifyFocus( ref ); + expect( listener ).toHaveBeenCalledWith( ref ); + } ); + + it( 'listens to blur event', () => { + const listener = jest.fn(); + addBlurListener( listener ); + notifyBlur( ref ); + expect( listener ).toHaveBeenCalledWith( ref ); + } ); + + it( 'does not call focus listener if removed', () => { + const listener = jest.fn(); + addFocusListener( listener ); + removeFocusListener( listener ); + notifyFocus( ref ); + expect( listener ).not.toHaveBeenCalled(); + } ); + + it( 'does not call blur listener if removed', () => { + const listener = jest.fn(); + addBlurListener( listener ); + removeBlurListener( listener ); + notifyBlur( ref ); + expect( listener ).not.toHaveBeenCalled(); + } ); + + it( 'returns true if an element is focused', () => { + notifyFocus( ref ); + expect( isFocused() ).toBeTruthy(); + } ); + + it( 'returns false if an element is unfocused', () => { + notifyFocus( ref ); + notifyBlur( ref ); + expect( isFocused() ).toBeFalsy(); + } ); + + it( 'returns current focused element', () => { + notifyFocus( ref ); + expect( getCurrentFocusedElement() ).toBe( ref ); + } ); + + it( 'returns null if focused element is unfocused', () => { + notifyFocus( ref ); + notifyBlur( ref ); + expect( getCurrentFocusedElement() ).toBe( null ); + } ); + + it( 'focus an element', () => { + focus( ref ); + expect( TextInputState.focusTextInput ).toHaveBeenCalledWith( ref ); + } ); + + it( 'unfocuses an element', () => { + blur( ref ); + expect( TextInputState.blurTextInput ).toHaveBeenCalledWith( ref ); + } ); +} ); From 39eaa57b27446166096a4facfe38e0de8b57bf4a Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 19:07:21 +0200 Subject: [PATCH 07/14] Update focus change listener logic --- .../block-draggable/index.native.js | 22 +----- .../react-native-aztec/src/AztecInputState.js | 76 +++++++------------ packages/react-native-aztec/src/AztecView.js | 4 +- .../src/test/AztecInputState.test.js | 66 ++++++++-------- 4 files changed, 65 insertions(+), 103 deletions(-) diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js index 14880d1fa5ea4..53fe23e7597be 100644 --- a/packages/block-editor/src/components/block-draggable/index.native.js +++ b/packages/block-editor/src/components/block-draggable/index.native.js @@ -319,28 +319,14 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { wasBeingDragged.current = isBeingDragged; }, [ isBeingDragged ] ); - const onFocusAztec = useCallback( () => { - setIsEditingText( true ); + const onFocusChangeAztec = useCallback( ( { isFocused } ) => { + setIsEditingText( isFocused ); }, [] ); - const onBlurAztec = useCallback( () => { - setIsEditingText( false ); - }, [] ); - - const registerAztecListeners = () => { - RCTAztecView.InputState.addFocusListener( onFocusAztec ); - RCTAztecView.InputState.addBlurListener( onBlurAztec ); - }; - - const unregisterAztecListeners = () => { - RCTAztecView.InputState.removeFocusListener( onFocusAztec ); - RCTAztecView.InputState.removeBlurListener( onBlurAztec ); - }; - useEffect( () => { - registerAztecListeners(); + RCTAztecView.InputState.addFocusChangeListener( onFocusChangeAztec ); return () => { - unregisterAztecListeners(); + RCTAztecView.InputState.removeFocusListener( onFocusChangeAztec ); }; }, [] ); diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 461c2290fc117..904e2cac1ce3e 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -5,51 +5,40 @@ import TextInputState from 'react-native/Libraries/Components/TextInput/TextInpu /** @typedef {import('@wordpress/element').RefObject} RefObject */ -const focusListeners = []; -const blurListeners = []; +const focusChangeListeners = []; let currentFocusedElement = null; /** - * Adds a listener that will be called when any Aztec view is focused. + * Adds a listener that will be called in the following cases: * - * @param {Function} listener - */ -export const addFocusListener = ( listener ) => { - focusListeners.push( listener ); -}; - -/** - * Adds a listener that will be called when any Aztec view is unfocused. + * - An Aztec view is being focused and all were previously unfocused. + * - An Aztec view is being unfocused and none will be focused. + * + * Note that this listener won't be called when switching focus between Aztec views. * * @param {Function} listener */ -export const addBlurListener = ( listener ) => { - blurListeners.push( listener ); +export const addFocusChangeListener = ( listener ) => { + focusChangeListeners.push( listener ); }; /** - * Removes a listener from the focus listeners list. + * Removes a listener from the focus change listeners list. * * @param {Function} listener */ -export const removeFocusListener = ( listener ) => { - const itemIndex = focusListeners.indexOf( listener ); +export const removeFocusChangeListener = ( listener ) => { + const itemIndex = focusChangeListeners.indexOf( listener ); if ( itemIndex !== -1 ) { - focusListeners.splice( itemIndex, 1 ); + focusChangeListeners.splice( itemIndex, 1 ); } }; -/** - * Removes a listener from the blur listeners list. - * - * @param {Function} listener - */ -export const removeBlurListener = ( listener ) => { - const itemIndex = blurListeners.indexOf( listener ); - if ( itemIndex !== -1 ) { - blurListeners.splice( itemIndex, 1 ); - } +const notifyListeners = ( { isFocused } ) => { + focusChangeListeners.forEach( ( listener ) => { + listener( { isFocused } ); + } ); }; /** @@ -71,29 +60,18 @@ export const getCurrentFocusedElement = () => { }; /** - * Notifies that an Aztec view is being focused. - * - * @param {RefObject} element Aztec view being focused. - */ -export const notifyFocus = ( element ) => { - currentFocusedElement = element; - - focusListeners.forEach( ( listener ) => { - listener( element ); - } ); -}; - -/** - * Notifies that an Aztec view is being unfocused. - * - * @param {RefObject} element Aztec view being unfocused. + * Notifies that an Aztec view is being focused or unfocused. */ -export const notifyBlur = ( element ) => { - currentFocusedElement = null; - - blurListeners.forEach( ( listener ) => { - listener( element ); - } ); +export const notifyInputChange = () => { + const focusedInput = TextInputState.currentlyFocusedInput(); + const hasAnyFocusedInput = focusedInput !== null; + if ( hasAnyFocusedInput && ! currentFocusedElement ) { + notifyListeners( { isFocused: true } ); + currentFocusedElement = focusedInput; + } else if ( ! hasAnyFocusedInput && currentFocusedElement ) { + currentFocusedElement = null; + notifyListeners( { isFocused: false } ); + } }; /** diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index 3ce618a2d407e..3e6193bf10a4b 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -122,7 +122,7 @@ class AztecView extends Component { } _onFocus( event ) { - AztecInputState.notifyFocus( this.aztecViewRef.current ); + AztecInputState.notifyInputChange(); if ( ! this.props.onFocus ) { return; @@ -136,7 +136,7 @@ class AztecView extends Component { this.selectionEndCaretY = null; AztecInputState.blur( this.aztecViewRef.current ); - AztecInputState.notifyBlur( this.aztecViewRef.current ); + AztecInputState.notifyInputChange(); if ( ! this.props.onBlur ) { return; diff --git a/packages/react-native-aztec/src/test/AztecInputState.test.js b/packages/react-native-aztec/src/test/AztecInputState.test.js index eaa9ef21f4957..6dd80394abbbc 100644 --- a/packages/react-native-aztec/src/test/AztecInputState.test.js +++ b/packages/react-native-aztec/src/test/AztecInputState.test.js @@ -7,16 +7,13 @@ import TextInputState from 'react-native/Libraries/Components/TextInput/TextInpu * Internal dependencies */ import { - addBlurListener, - addFocusListener, + addFocusChangeListener, getCurrentFocusedElement, isFocused, focus, blur, - notifyBlur, - notifyFocus, - removeFocusListener, - removeBlurListener, + notifyInputChange, + removeFocusChangeListener, } from '../AztecInputState'; jest.mock( @@ -24,61 +21,62 @@ jest.mock( () => ( { focusTextInput: jest.fn(), blurTextInput: jest.fn(), + currentlyFocusedInput: jest.fn(), } ) ); const ref = { current: null }; +const updateCurrentFocusedInput = ( value ) => { + TextInputState.currentlyFocusedInput.mockReturnValue( value ); + notifyInputChange(); +}; + describe( 'Aztec Input State', () => { - it( 'listens to focus event', () => { + it( 'listens to focus change event', () => { const listener = jest.fn(); - addFocusListener( listener ); - notifyFocus( ref ); - expect( listener ).toHaveBeenCalledWith( ref ); - } ); + addFocusChangeListener( listener ); - it( 'listens to blur event', () => { - const listener = jest.fn(); - addBlurListener( listener ); - notifyBlur( ref ); - expect( listener ).toHaveBeenCalledWith( ref ); - } ); + updateCurrentFocusedInput( ref ); - it( 'does not call focus listener if removed', () => { - const listener = jest.fn(); - addFocusListener( listener ); - removeFocusListener( listener ); - notifyFocus( ref ); - expect( listener ).not.toHaveBeenCalled(); + expect( listener ).toHaveBeenCalledWith( { isFocused: true } ); + + updateCurrentFocusedInput( null ); + + expect( listener ).toHaveBeenCalledWith( { isFocused: false } ); } ); - it( 'does not call blur listener if removed', () => { + it( 'does not call focus change listener if removed', () => { const listener = jest.fn(); - addBlurListener( listener ); - removeBlurListener( listener ); - notifyBlur( ref ); - expect( listener ).not.toHaveBeenCalled(); + addFocusChangeListener( listener ); + removeFocusChangeListener( listener ); + + updateCurrentFocusedInput( ref ); + + expect( listener ).not.toHaveBeenCalledWith( { isFocused: true } ); + + updateCurrentFocusedInput( null ); + + expect( listener ).not.toHaveBeenCalledWith( { isFocused: false } ); } ); it( 'returns true if an element is focused', () => { - notifyFocus( ref ); + updateCurrentFocusedInput( ref ); expect( isFocused() ).toBeTruthy(); } ); it( 'returns false if an element is unfocused', () => { - notifyFocus( ref ); - notifyBlur( ref ); + updateCurrentFocusedInput( null ); expect( isFocused() ).toBeFalsy(); } ); it( 'returns current focused element', () => { - notifyFocus( ref ); + updateCurrentFocusedInput( ref ); expect( getCurrentFocusedElement() ).toBe( ref ); } ); it( 'returns null if focused element is unfocused', () => { - notifyFocus( ref ); - notifyBlur( ref ); + updateCurrentFocusedInput( null ); expect( getCurrentFocusedElement() ).toBe( null ); } ); From 6f099cabc0831906a93a9095cfb885ad6ba5c581 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 20 Apr 2022 19:13:27 +0200 Subject: [PATCH 08/14] Update listen to focus change event test --- packages/react-native-aztec/src/test/AztecInputState.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react-native-aztec/src/test/AztecInputState.test.js b/packages/react-native-aztec/src/test/AztecInputState.test.js index 6dd80394abbbc..23bae7ab46f46 100644 --- a/packages/react-native-aztec/src/test/AztecInputState.test.js +++ b/packages/react-native-aztec/src/test/AztecInputState.test.js @@ -35,12 +35,17 @@ const updateCurrentFocusedInput = ( value ) => { describe( 'Aztec Input State', () => { it( 'listens to focus change event', () => { const listener = jest.fn(); + const anotherRef = { current: null }; addFocusChangeListener( listener ); updateCurrentFocusedInput( ref ); expect( listener ).toHaveBeenCalledWith( { isFocused: true } ); + updateCurrentFocusedInput( anotherRef ); + + expect( listener ).toHaveBeenCalledTimes( 1 ); + updateCurrentFocusedInput( null ); expect( listener ).toHaveBeenCalledWith( { isFocused: false } ); From 2a65a26718a6a954d76f05b38aa69dfb9f971d29 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 21 Apr 2022 10:17:07 +0200 Subject: [PATCH 09/14] Fix react-native-aztec module mock --- .../@wordpress/react-native-aztec/index.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/native/__mocks__/@wordpress/react-native-aztec/index.js b/test/native/__mocks__/@wordpress/react-native-aztec/index.js index 49ebf60786dc4..dfd2f21c0504b 100644 --- a/test/native/__mocks__/@wordpress/react-native-aztec/index.js +++ b/test/native/__mocks__/@wordpress/react-native-aztec/index.js @@ -9,9 +9,15 @@ import { omit } from 'lodash'; */ import { forwardRef } from '@wordpress/element'; +const reactNativeAztecMock = jest.createMockFromModule( + '@wordpress/react-native-aztec' +); +// Preserve the mock of AztecInputState to be exported with the AztecView mock. +const AztecInputState = reactNativeAztecMock.default.InputState; + const UNSUPPORTED_PROPS = [ 'style' ]; -const RCTAztecView = ( { accessibilityLabel, text, ...rest }, ref ) => { +const AztecView = ( { accessibilityLabel, text, ...rest }, ref ) => { return ( { ); }; -export default forwardRef( RCTAztecView ); +// Replace default mock of AztecView component with custom implementation. +reactNativeAztecMock.default = forwardRef( AztecView ); +reactNativeAztecMock.default.InputState = AztecInputState; + +module.exports = reactNativeAztecMock; From a1b63c7ce300ec19ff90718a21a78f2c95c5f0ec Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 21 Apr 2022 17:40:18 +0200 Subject: [PATCH 10/14] Fix wrong call to removeFocusChangeListener --- .../src/components/block-draggable/index.native.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js index 53fe23e7597be..3f27ecd6ab2d3 100644 --- a/packages/block-editor/src/components/block-draggable/index.native.js +++ b/packages/block-editor/src/components/block-draggable/index.native.js @@ -326,7 +326,9 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { useEffect( () => { RCTAztecView.InputState.addFocusChangeListener( onFocusChangeAztec ); return () => { - RCTAztecView.InputState.removeFocusListener( onFocusChangeAztec ); + RCTAztecView.InputState.removeFocusChangeListener( + onFocusChangeAztec + ); }; }, [] ); From e39337340c8d9f9aa7ff75134d5f7a2e992d1010 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 21 Apr 2022 17:41:54 +0200 Subject: [PATCH 11/14] Fix updating currentFocusedElement value --- packages/react-native-aztec/src/AztecInputState.js | 11 +++++++---- .../src/test/AztecInputState.test.js | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 904e2cac1ce3e..b21c4e6eee6fd 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -65,12 +65,15 @@ export const getCurrentFocusedElement = () => { export const notifyInputChange = () => { const focusedInput = TextInputState.currentlyFocusedInput(); const hasAnyFocusedInput = focusedInput !== null; - if ( hasAnyFocusedInput && ! currentFocusedElement ) { - notifyListeners( { isFocused: true } ); + + if ( hasAnyFocusedInput ) { + if ( ! currentFocusedElement ) { + notifyListeners( { isFocused: true } ); + } currentFocusedElement = focusedInput; - } else if ( ! hasAnyFocusedInput && currentFocusedElement ) { - currentFocusedElement = null; + } else if ( currentFocusedElement ) { notifyListeners( { isFocused: false } ); + currentFocusedElement = null; } }; diff --git a/packages/react-native-aztec/src/test/AztecInputState.test.js b/packages/react-native-aztec/src/test/AztecInputState.test.js index 23bae7ab46f46..4e5a59c6caf23 100644 --- a/packages/react-native-aztec/src/test/AztecInputState.test.js +++ b/packages/react-native-aztec/src/test/AztecInputState.test.js @@ -76,8 +76,12 @@ describe( 'Aztec Input State', () => { } ); it( 'returns current focused element', () => { + const anotherRef = { current: null }; updateCurrentFocusedInput( ref ); expect( getCurrentFocusedElement() ).toBe( ref ); + + updateCurrentFocusedInput( anotherRef ); + expect( getCurrentFocusedElement() ).toBe( anotherRef ); } ); it( 'returns null if focused element is unfocused', () => { From ee869865fdd0722430f07b2d1d8a190d6d7a87d6 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 21 Apr 2022 18:45:47 +0200 Subject: [PATCH 12/14] Check if an inner block is selected when enabling dragging --- .../src/components/block-draggable/index.native.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js index 3f27ecd6ab2d3..84a6e562b012e 100644 --- a/packages/block-editor/src/components/block-draggable/index.native.js +++ b/packages/block-editor/src/components/block-draggable/index.native.js @@ -291,6 +291,7 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { getTemplateLock, isBlockBeingDragged, getSelectedBlockClientId, + hasSelectedInnerBlock, } = _select( blockEditorStore ); const rootClientId = getBlockRootClientId( clientId ); const templateLock = rootClientId @@ -302,7 +303,9 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { isBeingDragged: isBlockBeingDragged( clientId ), isDraggable: 'all' !== templateLock, isBlockSelected: - selectedBlockClientId && selectedBlockClientId === clientId, + selectedBlockClientId && + ( selectedBlockClientId === clientId || + hasSelectedInnerBlock( clientId, true ) ), }; }, [ clientId ] From f8690dbc246a5b99bc7e0d20c4e31751442e14ff Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 26 Apr 2022 10:31:01 +0200 Subject: [PATCH 13/14] Wrap draggable long-press handler with useCallback --- .../src/components/block-draggable/index.native.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js index 84a6e562b012e..f291a20edb2f1 100644 --- a/packages/block-editor/src/components/block-draggable/index.native.js +++ b/packages/block-editor/src/components/block-draggable/index.native.js @@ -335,6 +335,11 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { }; }, [] ); + const onLongPressDraggable = useCallback( () => { + // Ensure that no text input is focused when starting the dragging gesture in order to prevent conflicts with text editing. + RCTAztecView.InputState.blurCurrentFocusedElement(); + }, [] ); + const animatedWrapperStyles = useAnimatedStyle( () => { return { opacity: draggingAnimation.opacity.value, @@ -365,10 +370,7 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => { : DEFAULT_LONG_PRESS_MIN_DURATION, android: DEFAULT_LONG_PRESS_MIN_DURATION, } ) } - onLongPress={ () => { - // Ensure that no text input is focused when starting the dragging gesture in order to prevent conflicts with text editing. - RCTAztecView.InputState.blurCurrentFocusedElement(); - } } + onLongPress={ onLongPressDraggable } > { children( { isDraggable: true } ) } From 8a2e0411d10a234bf819dbaa0d3d9b7568d3dd09 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 26 Apr 2022 10:31:35 +0200 Subject: [PATCH 14/14] Add documentation to notifyListeners function --- packages/react-native-aztec/src/AztecInputState.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index b21c4e6eee6fd..b0b184c2dd5c6 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -35,6 +35,12 @@ export const removeFocusChangeListener = ( listener ) => { } }; +/** + * Notifies listeners about changes in focus. + * + * @param {Object} event Event data to be notified to listeners. + * @param {boolean} event.isFocused True if any Aztec view is currently focused. + */ const notifyListeners = ( { isFocused } ) => { focusChangeListeners.forEach( ( listener ) => { listener( { isFocused } );