Skip to content

Commit

Permalink
[RNMobile] Fix issues related to editing text and dragging gesture (#…
Browse files Browse the repository at this point in the history
…40480)

* Add input state functionality to Aztec

* Control drag enabling with Aztec input state

* Force disabling text editing when dragging

* Add documentation to AztecInputState

* Update changelog

* Add tests for Aztec input state

* Update focus change listener logic

* Update listen to focus change event test

* Fix react-native-aztec module mock

* Fix wrong call to removeFocusChangeListener

* Fix updating currentFocusedElement value

* Check if an inner block is selected when enabling dragging

* Wrap draggable long-press handler with useCallback

* Add documentation to notifyListeners function
  • Loading branch information
fluiddot authored Apr 27, 2022
1 parent a4f52e3 commit 1320c03
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ),
Expand All @@ -275,27 +284,28 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => {
);
};

const { isDraggable, isBeingDragged, canDragBlock } = useSelect(
const { isDraggable, isBeingDragged, isBlockSelected } = useSelect(
( _select ) => {
const {
getBlockRootClientId,
getTemplateLock,
isBlockBeingDragged,
hasSelectedBlock,
getSelectedBlockClientId,
hasSelectedInnerBlock,
} = _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 ||
hasSelectedInnerBlock( clientId, true ) ),
};
},
[ clientId ]
Expand All @@ -312,6 +322,24 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => {
wasBeingDragged.current = isBeingDragged;
}, [ isBeingDragged ] );

const onFocusChangeAztec = useCallback( ( { isFocused } ) => {
setIsEditingText( isFocused );
}, [] );

useEffect( () => {
RCTAztecView.InputState.addFocusChangeListener( onFocusChangeAztec );
return () => {
RCTAztecView.InputState.removeFocusChangeListener(
onFocusChangeAztec
);
};
}, [] );

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,
Expand All @@ -322,6 +350,8 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => {
styles[ 'draggable-wrapper__container' ],
];

const canDragBlock = enabled && ( ! isBlockSelected || ! isEditingText );

if ( ! isDraggable ) {
return children( { isDraggable: false } );
}
Expand All @@ -340,6 +370,7 @@ const BlockDraggable = ( { clientId, children, enabled = true } ) => {
: DEFAULT_LONG_PRESS_MIN_DURATION,
android: DEFAULT_LONG_PRESS_MIN_DURATION,
} ) }
onLongPress={ onLongPressDraggable }
>
<Animated.View style={ wrapperStyles }>
{ children( { isDraggable: true } ) }
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-aztec/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
111 changes: 111 additions & 0 deletions packages/react-native-aztec/src/AztecInputState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* External dependencies
*/
import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState';

/** @typedef {import('@wordpress/element').RefObject} RefObject */

const focusChangeListeners = [];

let currentFocusedElement = null;

/**
* Adds a listener that will be called in the following cases:
*
* - 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 addFocusChangeListener = ( listener ) => {
focusChangeListeners.push( listener );
};

/**
* Removes a listener from the focus change listeners list.
*
* @param {Function} listener
*/
export const removeFocusChangeListener = ( listener ) => {
const itemIndex = focusChangeListeners.indexOf( listener );
if ( itemIndex !== -1 ) {
focusChangeListeners.splice( itemIndex, 1 );
}
};

/**
* 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 } );
} );
};

/**
* 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 or unfocused.
*/
export const notifyInputChange = () => {
const focusedInput = TextInputState.currentlyFocusedInput();
const hasAnyFocusedInput = focusedInput !== null;

if ( hasAnyFocusedInput ) {
if ( ! currentFocusedElement ) {
notifyListeners( { isFocused: true } );
}
currentFocusedElement = focusedInput;
} else if ( currentFocusedElement ) {
notifyListeners( { isFocused: false } );
currentFocusedElement = null;
}
};

/**
* 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() );
}
};
23 changes: 17 additions & 6 deletions packages/react-native-aztec/src/AztecView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -117,6 +122,8 @@ class AztecView extends Component {
}

_onFocus( event ) {
AztecInputState.notifyInputChange();

if ( ! this.props.onFocus ) {
return;
}
Expand All @@ -127,7 +134,9 @@ class AztecView extends Component {

_onBlur( event ) {
this.selectionEndCaretY = null;
TextInputState.blurTextInput( this.aztecViewRef.current );

AztecInputState.blur( this.aztecViewRef.current );
AztecInputState.notifyInputChange();

if ( ! this.props.onBlur ) {
return;
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -251,4 +260,6 @@ class AztecView extends Component {

const RCTAztecView = requireNativeComponent( 'RCTAztecView', AztecView );

AztecView.InputState = AztecInputState;

export default AztecView;
Loading

0 comments on commit 1320c03

Please sign in to comment.