Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mobile] Unselect blocks using the hardware back button (Android) #57279

Merged
merged 11 commits into from
Jan 2, 2024
Merged
17 changes: 17 additions & 0 deletions packages/edit-post/src/test/editor.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
screen,
setupCoreBlocks,
} from 'test/helpers';
import { BackHandler } from 'react-native';

/**
* WordPress dependencies
Expand Down Expand Up @@ -129,4 +130,20 @@ describe( 'Editor', () => {
// Assert
expect( getEditorHtml() ).toMatchSnapshot();
} );

it( 'unselects current block when tapping on the hardware back button', async () => {
// Arrange
await initializeEditor();
await addBlock( screen, 'Spacer' );

// Act
act( () => {
BackHandler.mockPressBack();
} );

// Assert
const openBlockSettingsButton =
screen.queryAllByLabelText( 'Open Settings' );
expect( openBlockSettingsButton.length ).toBe( 0 );
} );
} );
31 changes: 29 additions & 2 deletions packages/editor/src/components/provider/index.native.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { BackHandler } from 'react-native';
import memize from 'memize';
import { SafeAreaProvider } from 'react-native-safe-area-context';

Expand Down Expand Up @@ -79,6 +80,8 @@ class NativeEditorProvider extends Component {
this.post
);

this.onHardwareBackPress = this.onHardwareBackPress.bind( this );

this.getEditorSettings = memize(
( settings, capabilities ) => ( {
...settings,
Expand Down Expand Up @@ -191,6 +194,11 @@ class NativeEditorProvider extends Component {
this.setState( { isHelpVisible: true } );
} );

this.hardwareBackPressListener = BackHandler.addEventListener(
'hardwareBackPress',
this.onHardwareBackPress
);

// Request current block impressions from native app.
requestBlockTypeImpressions( ( storedImpressions ) => {
const impressions = { ...NEW_BLOCK_TYPES, ...storedImpressions };
Expand Down Expand Up @@ -250,6 +258,10 @@ class NativeEditorProvider extends Component {
if ( this.subscriptionParentShowEditorHelp ) {
this.subscriptionParentShowEditorHelp.remove();
}

if ( this.hardwareBackPressListener ) {
this.hardwareBackPressListener.remove();
}
}

getThemeColors( { rawStyles, rawFeatures } ) {
Expand Down Expand Up @@ -280,6 +292,16 @@ class NativeEditorProvider extends Component {
}
}

onHardwareBackPress() {
const { clearSelectedBlock, selectedBlockIndex } = this.props;

if ( selectedBlockIndex !== -1 ) {
clearSelectedBlock();
return true;
}
return false;
}

serializeToNativeAction() {
const title = this.props.title;
let html;
Expand Down Expand Up @@ -397,8 +419,12 @@ const ComposedNativeProvider = compose( [
withDispatch( ( dispatch ) => {
const { editPost, resetEditorBlocks, updateEditorSettings } =
dispatch( editorStore );
const { updateSettings, insertBlock, replaceBlock } =
dispatch( blockEditorStore );
const {
clearSelectedBlock,
updateSettings,
insertBlock,
replaceBlock,
} = dispatch( blockEditorStore );
const { switchEditorMode } = dispatch( editPostStore );
const { addEntities, receiveEntityRecords } = dispatch( coreStore );
const { createSuccessNotice, createErrorNotice } =
Expand All @@ -411,6 +437,7 @@ const ComposedNativeProvider = compose( [
insertBlock,
createSuccessNotice,
createErrorNotice,
clearSelectedBlock,
editTitle( title ) {
editPost( { title } );
},
Expand Down
29 changes: 17 additions & 12 deletions packages/react-native-aztec/src/AztecInputState.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const focus = ( element ) => {
// will take precedence and cancels pending blur events.
blur.cancel();
// Similar to blur events, we also need to cancel potential keyboard dismiss.
dismissKeyboardDebounce.cancel();
blurOnUnmountDebounce.cancel();

TextInputState.focusTextInput( element );
notifyInputChange();
Expand All @@ -164,13 +164,6 @@ 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 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
* 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.
*
Expand All @@ -181,13 +174,25 @@ export const blurOnUnmount = ( element ) => {
// If a blur event was triggered before unmount, we need to cancel them to avoid
// exceptions.
blur.cancel();
if ( Platform.OS === 'android' ) {
dismissKeyboardDebounce();
}
blurOnUnmountDebounce();
}
};

const dismissKeyboardDebounce = debounce( () => hideAndroidSoftKeyboard(), 0 );
// For updating the input state and dismissing the keyboard, we use debounce to avoid
// conflicts with the focus event when both are triggered at the same time.
const blurOnUnmountDebounce = debounce( () => {
// At this point, the text input will be destroyed but it's still focused. Hence, we
// have to explicitly notify listeners and update internal input state.
notifyListeners( { isFocused: false } );
currentFocusedElement = null;
fluiddot marked this conversation as resolved.
Show resolved Hide resolved

// 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.
if ( Platform.OS === 'android' ) {
hideAndroidSoftKeyboard();
}
}, 0 );

/**
* Unfocuses the current focused element.
Expand Down
13 changes: 13 additions & 0 deletions packages/react-native-aztec/src/test/AztecInputState.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isFocused,
focus,
blur,
blurOnUnmount,
notifyInputChange,
removeFocusChangeListener,
} from '../AztecInputState';
Expand Down Expand Up @@ -101,4 +102,16 @@ describe( 'Aztec Input State', () => {
jest.runAllTimers();
expect( TextInputState.blurTextInput ).toHaveBeenCalledWith( ref );
} );

it( 'unfocuses an element when unmounted', () => {
const listener = jest.fn();
addFocusChangeListener( listener );

updateCurrentFocusedInput( ref );
blurOnUnmount( ref );
jest.runAllTimers();

expect( listener ).toHaveBeenCalledWith( { isFocused: false } );
expect( isFocused() ).toBeFalsy();
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public class WPAndroidGlueCode {

private OnToggleRedoButtonListener mOnToggleRedoButtonListener;
private OnConnectionStatusEventListener mOnConnectionStatusEventListener;
private OnBackHandlerEventListener mOnBackHandlerEventListener;
private boolean mIsEditorMounted;

private String mContentHtml = "";
Expand All @@ -134,6 +135,7 @@ public class WPAndroidGlueCode {
private boolean mIsDarkMode;
private Consumer<Exception> mExceptionLogger;
private Consumer<String> mBreadcrumbLogger;
private boolean mShouldHandleBackPress = false;

public void onCreate(Context context) {
SoLoader.init(context, /* native exopackage */ false);
Expand All @@ -147,6 +149,10 @@ public boolean hasReactContext() {
return mReactContext != null;
}

public boolean shouldHandleBackPress() {
return mShouldHandleBackPress;
}

public boolean isContentChanged() {
return mContentChanged;
}
Expand Down Expand Up @@ -264,6 +270,10 @@ public interface OnConnectionStatusEventListener {
boolean onRequestConnectionStatus();
}

public interface OnBackHandlerEventListener {
void onBackHandler();
}

public void mediaSelectionCancelled() {
mAppendsMultipleSelectedToSiblingBlocks = false;
}
Expand Down Expand Up @@ -700,6 +710,7 @@ public void attachToContainer(ViewGroup viewGroup,
OnToggleUndoButtonListener onToggleUndoButtonListener,
OnToggleRedoButtonListener onToggleRedoButtonListener,
OnConnectionStatusEventListener onConnectionStatusEventListener,
OnBackHandlerEventListener onBackHandlerEventListener,
boolean isDarkMode) {
MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext();
contextWrapper.setBaseContext(viewGroup.getContext());
Expand All @@ -726,6 +737,7 @@ public void attachToContainer(ViewGroup viewGroup,
mOnToggleUndoButtonListener = onToggleUndoButtonListener;
mOnToggleRedoButtonListener = onToggleRedoButtonListener;
mOnConnectionStatusEventListener = onConnectionStatusEventListener;
mOnBackHandlerEventListener = onBackHandlerEventListener;

sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener);

Expand Down Expand Up @@ -761,6 +773,7 @@ private void refocus() {
}

public void onPause(Activity activity) {
mShouldHandleBackPress = false;
if (mReactInstanceManager != null) {
// get the focused view so we re-focus it later if needed. WeakReference so we don't leak it.
mLastFocusedView = new WeakReference<>(mReactRootView.findFocus());
Expand All @@ -770,25 +783,28 @@ public void onPause(Activity activity) {
}

public void onResume(final Fragment fragment, final Activity activity) {
mShouldHandleBackPress = true;
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostResume(activity,
new DefaultHardwareBackBtnHandler() {
@Override
public void invokeDefaultOnBackPressed() {
if (fragment.isAdded()) {
activity.onBackPressed();
mOnBackHandlerEventListener.onBackHandler();
}
}
});
}
}

public void onDetach(Activity activity) {
mShouldHandleBackPress = false;
mReactInstanceManager.onHostDestroy(activity);
mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().notifyModalClosed();
}

public void onDestroy(Activity activity) {
mShouldHandleBackPress = false;
Comment on lines +801 to +807
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: I debugged the app and seems onPause is called before detaching and destroying the activity, hence mShouldHandleBackPress would be always false at this point and we won't need to set it to false again. However, in case there might be other scenarios where onPause wouldn't be invoked, let's keep it as is.

if (mReactRootView != null) {
mReactRootView.unmountReactApplication();
mReactRootView = null;
Expand All @@ -804,6 +820,12 @@ public void onDestroy(Activity activity) {
}
}

public void onBackPressed() {
if (mReactInstanceManager != null) {
mReactInstanceManager.onBackPressed();
}
}

public void showDevOptionsDialog() {
mReactInstanceManager.showDevOptionsDialog();
}
Expand Down
11 changes: 10 additions & 1 deletion packages/react-native-bridge/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import { NativeModules, NativeEventEmitter, Platform } from 'react-native';

/**
* WordPress dependencies
*/
import RCTAztecView from '@wordpress/react-native-aztec';

const { RNReactNativeGutenbergBridge } = NativeModules;
const isIOS = Platform.OS === 'ios';
const isAndroid = Platform.OS === 'android';
Expand Down Expand Up @@ -489,7 +494,11 @@ export function showAndroidSoftKeyboard() {
return;
}

RNReactNativeGutenbergBridge.showAndroidSoftKeyboard();
const hasFocusedTextInput = RCTAztecView.InputState.isFocused();

if ( hasFocusedTextInput ) {
RNReactNativeGutenbergBridge.showAndroidSoftKeyboard();
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ For each user feature we should also add a importance categorization label to i
- [**] Fix regressions with wrapper props and font size customization [#56985]
- [***] Avoid keyboard dismiss when interacting with text blocks [#57070]
- [**] Auto-scroll upon block insertion [#57273]
- [*] Unselect blocks using the hardware back button (Android) [#57279]

## 1.109.3
- [**] Fix duplicate/unresponsive options in font size settings. [#56985]
Expand Down
6 changes: 6 additions & 0 deletions test/native/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,9 @@ jest.mock( '@wordpress/compose', () => {
jest.spyOn( Image, 'getSize' ).mockImplementation( ( url, success ) =>
success( 0, 0 )
);

jest.mock( 'react-native/Libraries/Utilities/BackHandler', () => {
return jest.requireActual(
'react-native/Libraries/Utilities/__mocks__/BackHandler.js'
);
} );
Loading