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
5 changes: 4 additions & 1 deletion packages/react-native-aztec/src/AztecInputState.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ export const blurOnUnmount = ( element ) => {
}
};

const dismissKeyboardDebounce = debounce( () => hideAndroidSoftKeyboard(), 0 );
const dismissKeyboardDebounce = debounce( () => {
hideAndroidSoftKeyboard();
currentFocusedElement = null;
fluiddot marked this conversation as resolved.
Show resolved Hide resolved
}, 0 );

/**
* Unfocuses the current focused element.
Expand Down
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
12 changes: 11 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,12 @@ export function showAndroidSoftKeyboard() {
return;
}

RNReactNativeGutenbergBridge.showAndroidSoftKeyboard();
const hasFocusedTextInput =
RCTAztecView.InputState.getCurrentFocusedElement() !== null;
fluiddot marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -17,6 +17,7 @@ For each user feature we should also add a importance categorization label to i
- [**] Fix crash when sharing unsupported media types on Android [#56791]
- [**] Fix regressions with wrapper props and font size customization [#56985]
- [***] Avoid keyboard dismiss when interacting with text blocks [#57070]
- [*] 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