From c428fd43302c38a8d0c945452c41235e64bd1607 Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco Date: Tue, 11 Jul 2023 14:58:09 +0200 Subject: [PATCH] [Mobile] - Move Undo/Redo buttons (#51766) * Mobile - Move Undo/Redo buttons * Mobile - Add undo/redo buttons and Toolbar to the Android demo app * Update Demo Android app icons * Update Demo iOS app icons and header * Android demo app - Update initial props * Update test util to use the menu options button instead of a keypress * Add mocks for newly added bridge functions and update editor history tests * Remove unneeded icons * Update color assets * Update Main activity to use default layout and add the toolbar programmatically due to issues with views and appium * Mobile E2E - Utils - Update to use the new menu button * Update Add block button ID for iOS * FIx navigation border bottom and icons insets * Update undo/redo buttons for RTL * Update undo/redo buttons when orientation changes, this is needed for Android's main host app activity * Revert: Update undo/redo buttons when orientation changes, this is needed for Android's main host app activity * Update changelog * E2E helpers - Update getAddBlockButton --- .../header/header-toolbar/index.native.js | 58 +++--- .../GutenbergBridgeJS2Parent.java | 4 + .../RNReactNativeGutenbergBridgeModule.java | 22 ++ .../WPAndroidGlue/WPAndroidGlueCode.java | 34 ++++ packages/react-native-bridge/index.js | 16 ++ .../react-native-bridge/ios/Gutenberg.swift | 8 + .../ios/GutenbergBridgeDelegate.swift | 4 + .../ios/RNReactNativeGutenbergBridge.m | 2 + .../ios/RNReactNativeGutenbergBridge.swift | 12 ++ packages/react-native-editor/CHANGELOG.md | 1 + .../__device-tests__/helpers/utils.js | 19 +- .../__device-tests__/pages/editor-page.js | 10 +- .../android/app/build.gradle | 1 + .../main/java/com/gutenberg/MainActivity.java | 192 +++++++++++++++--- .../java/com/gutenberg/MainApplication.java | 26 ++- .../src/main/res/drawable/more_vertical.xml | 9 + .../app/src/main/res/drawable/redo.xml | 9 + .../app/src/main/res/drawable/undo.xml | 9 + .../app/src/main/res/menu/toolbar_menu.xml | 21 ++ .../src/main/res/values-night/colors_dark.xml | 6 + .../app/src/main/res/values-night/styles.xml | 20 ++ .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 14 +- .../src/main/res/values/styles_toolbar.xml | 10 + .../ios/Colors.xcassets/Contents.json | 6 + .../HeaderLine.colorset/Contents.json | 38 ++++ .../Primary.colorset/Contents.json | 33 +++ .../GutenbergDemo.xcodeproj/project.pbxproj | 4 + .../GutenbergViewController.swift | 113 ++++++++++- .../Images.xcassets/Contents.json | 4 +- .../more.imageset/Contents.json | 23 +++ .../more.imageset/editor-more.png | Bin 0 -> 94 bytes .../more.imageset/editor-more@2x.png | Bin 0 -> 128 bytes .../more.imageset/editor-more@3x.png | Bin 0 -> 181 bytes .../redo.imageset/Contents.json | 23 +++ .../Images.xcassets/redo.imageset/redo.png | Bin 0 -> 161 bytes .../Images.xcassets/redo.imageset/redo@2x.png | Bin 0 -> 241 bytes .../Images.xcassets/redo.imageset/redo@3x.png | Bin 0 -> 312 bytes .../undo.imageset/Contents.json | 23 +++ .../Images.xcassets/undo.imageset/undo.png | Bin 0 -> 167 bytes .../Images.xcassets/undo.imageset/undo@2x.png | Bin 0 -> 245 bytes .../Images.xcassets/undo.imageset/undo@3x.png | Bin 0 -> 310 bytes .../integration/editor-history.native.js | 52 +++-- test/native/setup.js | 4 + 45 files changed, 733 insertions(+), 106 deletions(-) create mode 100644 packages/react-native-editor/android/app/src/main/res/drawable/more_vertical.xml create mode 100644 packages/react-native-editor/android/app/src/main/res/drawable/redo.xml create mode 100644 packages/react-native-editor/android/app/src/main/res/drawable/undo.xml create mode 100644 packages/react-native-editor/android/app/src/main/res/menu/toolbar_menu.xml create mode 100644 packages/react-native-editor/android/app/src/main/res/values-night/colors_dark.xml create mode 100644 packages/react-native-editor/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/react-native-editor/android/app/src/main/res/values/colors.xml create mode 100644 packages/react-native-editor/android/app/src/main/res/values/styles_toolbar.xml create mode 100644 packages/react-native-editor/ios/Colors.xcassets/Contents.json create mode 100644 packages/react-native-editor/ios/Colors.xcassets/HeaderLine.colorset/Contents.json create mode 100644 packages/react-native-editor/ios/Colors.xcassets/Primary.colorset/Contents.json create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/Contents.json create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/editor-more.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/editor-more@2x.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/editor-more@3x.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/Contents.json create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/redo.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/redo@2x.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/redo@3x.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/undo.imageset/Contents.json create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/undo.imageset/undo.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/undo.imageset/undo@2x.png create mode 100644 packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/undo.imageset/undo@3x.png diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js index 6c5ae1b932e85..f526cfd232623 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.native.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js @@ -6,7 +6,7 @@ import { Platform, ScrollView, View } from 'react-native'; /** * WordPress dependencies */ -import { useCallback, useRef, useState } from '@wordpress/element'; +import { useCallback, useRef, useState, useEffect } from '@wordpress/element'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; @@ -23,11 +23,15 @@ import { media as imageIcon, video as videoIcon, gallery as galleryIcon, - undo as undoIcon, - redo as redoIcon, } from '@wordpress/icons'; import { store as editorStore } from '@wordpress/editor'; import { createBlock } from '@wordpress/blocks'; +import { + toggleUndoButton, + toggleRedoButton, + subscribeOnUndoPressed, + subscribeOnRedoPressed, +} from '@wordpress/react-native-bridge'; /** * Internal dependencies @@ -53,6 +57,24 @@ function HeaderToolbar( { const wasNoContentSelected = useRef( noContentSelected ); const [ isInserterOpen, setIsInserterOpen ] = useState( false ); + useEffect( () => { + const onUndoSubscription = subscribeOnUndoPressed( undo ); + const onRedoSubscription = subscribeOnRedoPressed( redo ); + + return () => { + onUndoSubscription?.remove(); + onRedoSubscription?.remove(); + }; + }, [ undo, redo ] ); + + useEffect( () => { + toggleUndoButton( ! hasUndo ); + }, [ hasUndo ] ); + + useEffect( () => { + toggleRedoButton( ! hasRedo ); + }, [ hasRedo ] ); + const scrollViewRef = useRef( null ); const scrollToStart = () => { // scrollview doesn't seem to automatically adjust to RTL on Android so, scroll to end when Android @@ -64,34 +86,6 @@ function HeaderToolbar( { } }; - const renderHistoryButtons = () => { - const buttons = [ - /* TODO: replace with EditorHistoryRedo and EditorHistoryUndo. */ - , - , - ]; - - return isRTL ? buttons.reverse() : buttons; - }; - const onInsertBlock = useCallback( ( blockType ) => () => { insertBlock( createBlock( blockType ), undefined, undefined, true, { @@ -195,9 +189,7 @@ function HeaderToolbar( { useExpandedMode={ useExpandedMode } onToggle={ onToggleInserter } /> - { noContentSelected && renderMediaButtons } - { renderHistoryButtons() } = Build.VERSION_CODES.Q) { diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index e270f91dbcae1..0e8962cc70482 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -109,6 +109,10 @@ public class WPAndroidGlueCode { private OnBlockTypeImpressionsEventListener mOnBlockTypeImpressionsEventListener; private OnCustomerSupportOptionsListener mOnCustomerSupportOptionsListener; private OnSendEventToHostListener mOnSendEventToHostListener; + + private OnToggleUndoButtonListener mOnToggleUndoButtonListener; + + private OnToggleRedoButtonListener mOnToggleRedoButtonListener; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -248,6 +252,14 @@ public interface OnSendEventToHostListener { void onSendEventToHost(String eventName, Map properties); } + public interface OnToggleUndoButtonListener { + void onToggleUndoButton(boolean isDisabled); + } + + public interface OnToggleRedoButtonListener { + void onToggleRedoButton(boolean isDisabled); + } + public void mediaSelectionCancelled() { mAppendsMultipleSelectedToSiblingBlocks = false; } @@ -573,6 +585,16 @@ public void requestGotoCustomerSupportOptions() { public void sendEventToHost(String eventName, ReadableMap properties) { mOnSendEventToHostListener.onSendEventToHost(eventName, properties.toHashMap()); } + + @Override + public void toggleUndoButton(boolean isDisabled) { + mOnToggleUndoButtonListener.onToggleUndoButton(isDisabled); + } + + @Override + public void toggleRedoButton(boolean isDisabled) { + mOnToggleRedoButtonListener.onToggleRedoButton(isDisabled); + } }, mIsDarkMode); return Arrays.asList( @@ -666,6 +688,8 @@ public void attachToContainer(ViewGroup viewGroup, OnBlockTypeImpressionsEventListener onBlockTypeImpressionsEventListener, OnCustomerSupportOptionsListener onCustomerSupportOptionsListener, OnSendEventToHostListener onSendEventToHostListener, + OnToggleUndoButtonListener onToggleUndoButtonListener, + OnToggleRedoButtonListener onToggleRedoButtonListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); @@ -689,6 +713,8 @@ public void attachToContainer(ViewGroup viewGroup, mOnBlockTypeImpressionsEventListener = onBlockTypeImpressionsEventListener; mOnCustomerSupportOptionsListener = onCustomerSupportOptionsListener; mOnSendEventToHostListener = onSendEventToHostListener; + mOnToggleUndoButtonListener = onToggleUndoButtonListener; + mOnToggleRedoButtonListener = onToggleRedoButtonListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -819,6 +845,14 @@ public void showEditorHelp() { mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().showEditorHelp(); } + public void onUndoPressed() { + mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onUndoPressed(); + } + + public void onRedoPressed() { + mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onRedoPressed(); + } + public void setTitle(String title) { mTitleInitialized = true; mTitle = title; diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index d432f0de9e238..89f9f029901f9 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -177,6 +177,14 @@ export function subscribeShowEditorHelp( callback ) { return gutenbergBridgeEvents.addListener( 'showEditorHelp', callback ); } +export function subscribeOnUndoPressed( callback ) { + return gutenbergBridgeEvents.addListener( 'onUndoPressed', callback ); +} + +export function subscribeOnRedoPressed( callback ) { + return gutenbergBridgeEvents.addListener( 'onRedoPressed', callback ); +} + /** * Request media picker for the given media source. * @@ -466,4 +474,12 @@ export function generateHapticFeedback() { RNReactNativeGutenbergBridge.generateHapticFeedback(); } +export function toggleUndoButton( isDisabled ) { + RNReactNativeGutenbergBridge.toggleUndoButton( isDisabled ); +} + +export function toggleRedoButton( isDisabled ) { + RNReactNativeGutenbergBridge.toggleRedoButton( isDisabled ); +} + export default RNReactNativeGutenbergBridge; diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index adf0eb667f976..4175c1e2343c3 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -201,6 +201,14 @@ public class Gutenberg: UIResponder { public func showEditorHelp() { bridgeModule.sendEventIfNeeded(.showEditorHelp, body: nil) } + + public func onUndoPressed() { + bridgeModule.sendEventIfNeeded(.onUndoPressed, body: nil) + } + + public func onRedoPressed() { + bridgeModule.sendEventIfNeeded(.onRedoPressed, body: nil) + } private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] { var settingsUpdates = [String : Any]() diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index 4b13e4e6a15a9..83d087bccab9d 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -279,6 +279,10 @@ public protocol GutenbergBridgeDelegate: AnyObject { /// Tells the delegate the editor requested sending an event func gutenbergDidRequestSendEventToHost(_ eventName: String, properties: [AnyHashable: Any]) + + func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) + + func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) } // MARK: - Optional GutenbergBridgeDelegate methods diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index c1990035b776c..d333f8c1722ad 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -40,5 +40,7 @@ @interface RCT_EXTERN_MODULE(RNReactNativeGutenbergBridge, NSObject) RCT_EXTERN_METHOD(requestGotoCustomerSupportOptions) RCT_EXTERN_METHOD(sendEventToHost:(NSString)eventName properties:(NSDictionary *)properties) RCT_EXTERN_METHOD(generateHapticFeedback) +RCT_EXTERN_METHOD(toggleUndoButton:(BOOL)isDisabled) +RCT_EXTERN_METHOD(toggleRedoButton:(BOOL)isDisabled) @end diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index bfc3507732138..8cf4f685bd22c 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -411,6 +411,16 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { func generateHapticFeedback() { UISelectionFeedbackGenerator().selectionChanged() } + + @objc + func toggleUndoButton(_ isDisabled: Bool) { + self.delegate?.gutenbergDidRequestToggleUndoButton(isDisabled) + } + + @objc + func toggleRedoButton(_ isDisabled: Bool) { + self.delegate?.gutenbergDidRequestToggleRedoButton(isDisabled) + } } // MARK: - RCTBridgeModule delegate @@ -438,6 +448,8 @@ extension RNReactNativeGutenbergBridge { case showNotice case mediaSave case showEditorHelp + case onUndoPressed + case onRedoPressed } public override func supportedEvents() -> [String]! { diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 2380e16ddb426..74faac652f07c 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [**] Add media inserter buttons to editor toolbar [#51827] - [**] Update native BlockOutline component styles to remove blue border from blocks [#51222] +- [**] Move the undo/redo buttons to the navigation bar [#51766] ## 1.99.0 - [*] Rename "Reusable blocks" to "Synced patterns", aligning with the web editor. [#51704] diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index 4591cd4176d25..1ad311ea55d4a 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -476,18 +476,21 @@ const dragAndDropAfterElement = async ( driver, element, nextElement ) => { const toggleHtmlMode = async ( driver, toggleOn ) => { if ( isAndroid() ) { - // Hit the "Menu" key. - await driver.pressKeycode( 82 ); + const moreOptionsButton = await driver.elementByAccessibilityId( + 'More options' + ); + await moreOptionsButton.click(); const showHtmlButtonXpath = '/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ListView/android.widget.TextView[9]'; await clickIfClickable( driver, showHtmlButtonXpath ); } else if ( toggleOn ) { - await clickIfClickable( - driver, - '//XCUIElementTypeButton[@name="..."]' + const moreOptionsButton = await driver.elementByAccessibilityId( + 'editor-menu-button' ); + await moreOptionsButton.click(); + await clickIfClickable( driver, '//XCUIElementTypeButton[@name="Switch to HTML"]' @@ -495,10 +498,10 @@ const toggleHtmlMode = async ( driver, toggleOn ) => { } else { // This is to wait for the clipboard paste notification to disappear, currently it overlaps with the menu button await driver.sleep( 3000 ); - await clickIfClickable( - driver, - '//XCUIElementTypeButton[@name="..."]' + const moreOptionsButton = await driver.elementByAccessibilityId( + 'editor-menu-button' ); + await moreOptionsButton.click(); await clickIfClickable( driver, '//XCUIElementTypeButton[@name="Switch To Visual"]' diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index f03dae92c4174..e0965e3394d93 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -26,7 +26,7 @@ const { const ADD_BLOCK_ID = isAndroid() ? 'Add block, Double tap to add a block' - : 'Add block'; + : 'add-block-button'; const initializeEditorPage = async () => { const driver = await setupDriver(); @@ -71,11 +71,11 @@ class EditorPage { return await this.driver.hasElementByAccessibilityId( 'block-list' ); } - async getAddBlockButton( options = { timeout: 3000 } ) { - return await this.waitForElementToBeDisplayedById( - ADD_BLOCK_ID, - options.timeout + async getAddBlockButton() { + const elements = await this.driver.elementsByAccessibilityId( + ADD_BLOCK_ID ); + return elements[ 0 ]; } // =============================== diff --git a/packages/react-native-editor/android/app/build.gradle b/packages/react-native-editor/android/app/build.gradle index c91c3c0723819..c388f935c1d14 100644 --- a/packages/react-native-editor/android/app/build.gradle +++ b/packages/react-native-editor/android/app/build.gradle @@ -241,6 +241,7 @@ dependencies { implementation "org.wordpress-mobile.gutenberg-mobile:react-native-bridge" implementation 'androidx.appcompat:appcompat:1.2.0' + implementation "com.google.android.material:material:1.9.0" //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:${extractPackageVersion(packageJson, 'react-native', 'dependencies')}" diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java index 2c31c3bdc8281..7e726ec3a88ef 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java @@ -1,17 +1,152 @@ package com.gutenberg; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; -import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; import com.facebook.react.ReactActivity; -import com.facebook.react.ReactActivityDelegate; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; import org.wordpress.mobile.WPAndroidGlue.GutenbergProps; import java.util.Locale; public class MainActivity extends ReactActivity { + private static MainActivity currentInstance; + + private ReactRootView mReactRootView; + private Menu mMenu; + + private void openReactNativeDebugMenu() { + ReactInstanceManager devSettingsModule = getReactInstanceManager(); + if (devSettingsModule != null) { + devSettingsModule.showDevOptionsDialog(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + mMenu = menu; + getMenuInflater().inflate(R.menu.toolbar_menu, menu); + + // Set opacity for menu items + MenuItem undoItem = menu.findItem(R.id.menuUndo); + undoItem.getIcon().setAlpha(76); + undoItem.setEnabled(false); + + MenuItem redoItem = menu.findItem(R.id.menuRedo); + redoItem.getIcon().setAlpha(76); + redoItem.setEnabled(false); + return true; + } + + public void updateUndoItem(boolean isDisabled) { + if (mMenu != null) { + runOnUiThread(new Runnable() { + @Override + public void run() { + MenuItem undoItem = mMenu.findItem(R.id.menuUndo); + + undoItem.setEnabled(!isDisabled); + undoItem.getIcon().setAlpha(!isDisabled ? 255 : 76); + } + }); + } + } + + public void updateRedoItem(boolean isDisabled) { + if (mMenu != null) { + runOnUiThread(new Runnable() { + @Override + public void run() { + MenuItem redoItem = mMenu.findItem(R.id.menuRedo); + + redoItem.setEnabled(!isDisabled); + redoItem.getIcon().setAlpha(!isDisabled ? 255 : 76); + } + }); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + MainApplication mainApplication = (MainApplication) getApplication(); + + int itemId = item.getItemId(); + if (itemId == R.id.menuUndo) { + mainApplication.toggleUndo(); + return true; + } + if (itemId == R.id.menuRedo) { + mainApplication.toggleRedo(); + return true; + } + if (itemId == R.id.menuButton) { + openReactNativeDebugMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + public static MainActivity getInstance() { + return currentInstance; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + currentInstance = this; + + // Create a LinearLayout that will hold both the toolbar and React Native content + LinearLayout linearLayout = new LinearLayout(this); + linearLayout.setOrientation(LinearLayout.VERTICAL); + linearLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Create a Toolbar instance + Toolbar toolbar = new Toolbar(this); + + // Set toolbar properties (you can customize this as you want) + toolbar.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + // Set the toolbar as the Activity's action bar + setSupportActionBar(toolbar); + + // Add the toolbar to the linear layout + linearLayout.addView(toolbar); + + // Create a View to be used as the border + View borderView = new View(this); + borderView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); + borderView.setBackgroundColor(ContextCompat.getColor(this, R.color.toolbarBorder)); + + // Add the border view to the linear layout + linearLayout.addView(borderView); + + // Create a ReactRootView and assign it to mReactRootView + mReactRootView = new ReactRootView(this); + LinearLayout.LayoutParams reactViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1); + mReactRootView.setLayoutParams(reactViewParams); + + // Add ReactView to the linear layout + linearLayout.addView(mReactRootView); + + // Set the linear layout as the content view + setContentView(linearLayout); + + // Load the React application + mReactRootView.startReactApplication( + ((MainApplication) getApplication()).getReactNativeHost().getReactInstanceManager(), + getMainComponentName(), + getAppOptions() + ); + } /** * Returns the name of the main component registered from JavaScript. @@ -22,35 +157,28 @@ protected String getMainComponentName() { return "gutenberg"; } - @Override - protected ReactActivityDelegate createReactActivityDelegate() { - return new ReactActivityDelegate(this, getMainComponentName()) { - @Nullable - @Override - protected Bundle getLaunchOptions() { - Bundle bundle = new Bundle(); - - // Add locale - String languageString = Locale.getDefault().toString(); - String localeSlug = languageString.replace("_", "-").toLowerCase(Locale.ENGLISH); - bundle.putString(GutenbergProps.PROP_LOCALE, localeSlug); - - // Add capabilities - Bundle capabilities = new Bundle(); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_MENTIONS, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_XPOSTS, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_REUSABLE_BLOCK, false); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_IS_AUDIO_BLOCK_MEDIA_UPLOAD_ENABLED, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_TILED_GALLERY_BLOCK, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_VIDEOPRESS_BLOCK, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_FACEBOOK_EMBED_BLOCK, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_INSTAGRAM_EMBED_BLOCK, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_LOOM_EMBED_BLOCK, true); - capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_SMARTFRAME_EMBED_BLOCK, true); - bundle.putBundle(GutenbergProps.PROP_CAPABILITIES, capabilities); - return bundle; - } - }; + private Bundle getAppOptions() { + Bundle bundle = new Bundle(); + + // Add locale + String languageString = Locale.getDefault().toString(); + String localeSlug = languageString.replace("_", "-").toLowerCase(Locale.ENGLISH); + bundle.putString(GutenbergProps.PROP_LOCALE, localeSlug); + + // Add capabilities + Bundle capabilities = new Bundle(); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_MENTIONS, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_XPOSTS, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_REUSABLE_BLOCK, false); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_IS_AUDIO_BLOCK_MEDIA_UPLOAD_ENABLED, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_TILED_GALLERY_BLOCK, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_VIDEOPRESS_BLOCK, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_FACEBOOK_EMBED_BLOCK, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_INSTAGRAM_EMBED_BLOCK, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_LOOM_EMBED_BLOCK, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_SMARTFRAME_EMBED_BLOCK, true); + bundle.putBundle(GutenbergProps.PROP_CAPABILITIES, capabilities); + return bundle; } } diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index b781357bee369..8d9ee5ea2755e 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -1,7 +1,5 @@ package com.gutenberg; -import static org.wordpress.mobile.WPAndroidGlue.Media.createRNMediaUsingMimeType; - import android.app.Application; import android.content.Context; import android.content.Intent; @@ -296,6 +294,22 @@ public void requestGotoCustomerSupportOptions() { public void sendEventToHost(final String eventName, final ReadableMap properties) { Log.d("SendEventToHost", String.format("Gutenberg requested sending '%s' event to host with properties: %s", eventName, properties)); } + + @Override + public void toggleUndoButton(boolean isDisabled) { + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.updateUndoItem(isDisabled); + } + } + + @Override + public void toggleRedoButton(boolean isDisabled) { + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.updateRedoItem(isDisabled); + } + } }, isDarkMode()); return new ReactNativeHost(this) { @@ -341,6 +355,14 @@ private boolean isDarkMode() { return currentNightMode == Configuration.UI_MODE_NIGHT_YES; } + public void toggleUndo() { + mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onUndoPressed(); + } + + public void toggleRedo() { + mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onRedoPressed(); + } + private void openGutenbergWebView(String content, String blockId, String blockName) { diff --git a/packages/react-native-editor/android/app/src/main/res/drawable/more_vertical.xml b/packages/react-native-editor/android/app/src/main/res/drawable/more_vertical.xml new file mode 100644 index 0000000000000..0a839eda9c3af --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/drawable/more_vertical.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/react-native-editor/android/app/src/main/res/drawable/redo.xml b/packages/react-native-editor/android/app/src/main/res/drawable/redo.xml new file mode 100644 index 0000000000000..062f14eb68526 --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/drawable/redo.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/react-native-editor/android/app/src/main/res/drawable/undo.xml b/packages/react-native-editor/android/app/src/main/res/drawable/undo.xml new file mode 100644 index 0000000000000..773dbf271cc88 --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/drawable/undo.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/react-native-editor/android/app/src/main/res/menu/toolbar_menu.xml b/packages/react-native-editor/android/app/src/main/res/menu/toolbar_menu.xml new file mode 100644 index 0000000000000..6cc6c5213392d --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/menu/toolbar_menu.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/packages/react-native-editor/android/app/src/main/res/values-night/colors_dark.xml b/packages/react-native-editor/android/app/src/main/res/values-night/colors_dark.xml new file mode 100644 index 0000000000000..7ae0b9e788f00 --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/values-night/colors_dark.xml @@ -0,0 +1,6 @@ + + #000000 + #000000 + #FFFFFF + #60FFFFFF + diff --git a/packages/react-native-editor/android/app/src/main/res/values-night/styles.xml b/packages/react-native-editor/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000000..b8af6a1900713 --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/packages/react-native-editor/android/app/src/main/res/values/colors.xml b/packages/react-native-editor/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000000..0f702bc1ea645 --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #FFFFFF + #FFFFFF + #000000 + #3C3C435C + diff --git a/packages/react-native-editor/android/app/src/main/res/values/strings.xml b/packages/react-native-editor/android/app/src/main/res/values/strings.xml index 29e2e4d13944c..df039af86cc8c 100644 --- a/packages/react-native-editor/android/app/src/main/res/values/strings.xml +++ b/packages/react-native-editor/android/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ Gutenberg + Undo + Redo + More options diff --git a/packages/react-native-editor/android/app/src/main/res/values/styles.xml b/packages/react-native-editor/android/app/src/main/res/values/styles.xml index 7ba83a2ad5a2c..1423d4e65c673 100644 --- a/packages/react-native-editor/android/app/src/main/res/values/styles.xml +++ b/packages/react-native-editor/android/app/src/main/res/values/styles.xml @@ -1,9 +1,19 @@ - - diff --git a/packages/react-native-editor/android/app/src/main/res/values/styles_toolbar.xml b/packages/react-native-editor/android/app/src/main/res/values/styles_toolbar.xml new file mode 100644 index 0000000000000..dddd75b88b150 --- /dev/null +++ b/packages/react-native-editor/android/app/src/main/res/values/styles_toolbar.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/packages/react-native-editor/ios/Colors.xcassets/Contents.json b/packages/react-native-editor/ios/Colors.xcassets/Contents.json new file mode 100644 index 0000000000000..d458f1c5928f7 --- /dev/null +++ b/packages/react-native-editor/ios/Colors.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/packages/react-native-editor/ios/Colors.xcassets/HeaderLine.colorset/Contents.json b/packages/react-native-editor/ios/Colors.xcassets/HeaderLine.colorset/Contents.json new file mode 100644 index 0000000000000..5524b9950ee4c --- /dev/null +++ b/packages/react-native-editor/ios/Colors.xcassets/HeaderLine.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "0.360", + "blue": "0.025", + "green": "0.025", + "red": "0.025" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "0.360", + "blue": "0.025", + "green": "0.025", + "red": "0.025" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/packages/react-native-editor/ios/Colors.xcassets/Primary.colorset/Contents.json b/packages/react-native-editor/ios/Colors.xcassets/Primary.colorset/Contents.json new file mode 100644 index 0000000000000..7437866da3cfe --- /dev/null +++ b/packages/react-native-editor/ios/Colors.xcassets/Primary.colorset/Contents.json @@ -0,0 +1,33 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.000", + "green": "0.000", + "red": "0.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "platform": "ios", + "reference": "labelColor" + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj b/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj index 79a27e1e61846..f9caff24fac18 100644 --- a/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj +++ b/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1E4F2E752459E6F200EB73E7 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E85944D2449D85A006CC6A0 /* WebViewController.swift */; }; 1EFFAB71253EF6580062051E /* DocumentsMediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFFAB70253EF6580062051E /* DocumentsMediaSource.swift */; }; 2F634FA02731D5ED00310CC3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2F634F9F2731D5ED00310CC3 /* LaunchScreen.storyboard */; }; + 561700D72A4C91E700E7CF18 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 561700D62A4C91E700E7CF18 /* Colors.xcassets */; }; 6EBC6CA237E4D4B00D5AC79F /* Pods_GutenbergDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB79EC55FB340834C8D3BAB6 /* Pods_GutenbergDemo.framework */; }; 7EC7328F21907E3F00FED2E6 /* GutenbergViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC7328E21907E3F00FED2E6 /* GutenbergViewController.swift */; }; E8649334C74E9AB9AF90B020 /* Pods_GutenbergDemo_GutenbergDemoTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C4D4EB8326260FEFE8844CC /* Pods_GutenbergDemo_GutenbergDemoTests.framework */; }; @@ -49,6 +50,7 @@ 1EFFAB70253EF6580062051E /* DocumentsMediaSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsMediaSource.swift; sourceTree = ""; }; 2F634F9F2731D5ED00310CC3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 4EC14D7D4E04A462CB8AC230 /* Pods-GutenbergDemo-GutenbergDemoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GutenbergDemo-GutenbergDemoTests.release.xcconfig"; path = "Target Support Files/Pods-GutenbergDemo-GutenbergDemoTests/Pods-GutenbergDemo-GutenbergDemoTests.release.xcconfig"; sourceTree = ""; }; + 561700D62A4C91E700E7CF18 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 66F6B74F51BD6921D3AF25F6 /* Pods-GutenbergDemoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GutenbergDemoTests.debug.xcconfig"; path = "Target Support Files/Pods-GutenbergDemoTests/Pods-GutenbergDemoTests.debug.xcconfig"; sourceTree = ""; }; 6C4D4EB8326260FEFE8844CC /* Pods_GutenbergDemo_GutenbergDemoTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GutenbergDemo_GutenbergDemoTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 71AC74DFA49CB3BF62D440DB /* Pods-GutenbergDemoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GutenbergDemoTests.release.xcconfig"; path = "Target Support Files/Pods-GutenbergDemoTests/Pods-GutenbergDemoTests.release.xcconfig"; sourceTree = ""; }; @@ -124,6 +126,7 @@ FF6836C722035EAB00A0C562 /* MediaUploadCoordinator.swift */, F151983A2100DC3D000F6E97 /* MediaProvider.swift */, 13B07FB51A68108700A75B9A /* Images.xcassets */, + 561700D62A4C91E700E7CF18 /* Colors.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, FF83DAA82226905A00A34C93 /* CustomImageLoader.swift */, 2F634F9F2731D5ED00310CC3 /* LaunchScreen.storyboard */, @@ -302,6 +305,7 @@ F1EE6F7A21E7F0A500241744 /* NotoSerif-Italic.ttf in Resources */, 2F634FA02731D5ED00310CC3 /* LaunchScreen.storyboard in Resources */, F1EE6F7821E7F0A500241744 /* NotoSerif-BoldItalic.ttf in Resources */, + 561700D72A4C91E700E7CF18 /* Colors.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index 8b86369126fa5..1d0f3d79a527d 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -39,6 +39,61 @@ class GutenbergViewController: UIViewController { @objc func saveButtonPressed(sender: UIBarButtonItem) { gutenberg.requestHTML() } + + lazy var undoButton: UIButton = { + let isRTL = UIView.userInterfaceLayoutDirection(for: .unspecified) == .rightToLeft + let undoImage = UIImage(named: "undo") + let button = UIButton(type: .system) + button.setImage(isRTL ? undoImage?.withHorizontallyFlippedOrientation() : undoImage, for: .normal) + button.accessibilityIdentifier = "editor-undo-button" + button.accessibilityLabel = "Undo" + button.accessibilityHint = "Double tap to undo last change" + button.addTarget(self, action: #selector(undoButtonPressed(sender:)), for: .touchUpInside) + button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + button.sizeToFit() + button.alpha = 0.3 + button.isUserInteractionEnabled = false + return button + }() + + lazy var redoButton: UIButton = { + let isRTL = UIView.userInterfaceLayoutDirection(for: .unspecified) == .rightToLeft + let redoImage = UIImage(named: "redo") + let button = UIButton(type: .system) + button.setImage(isRTL ? redoImage?.withHorizontallyFlippedOrientation() : redoImage, for: .normal) + button.accessibilityIdentifier = "editor-redo-button" + button.accessibilityLabel = "Redo" + button.accessibilityHint = "Double tap to redo last change" + button.addTarget(self, action: #selector(redoButtonPressed(sender:)), for: .touchUpInside) + button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + button.sizeToFit() + button.alpha = 0.3 + button.isUserInteractionEnabled = false + return button + }() + + lazy var moreButton: UIButton = { + let moreImage = UIImage(named: "more") + let button = UIButton(type: .system) + button.setImage(moreImage, for: .normal) + button.titleLabel?.minimumScaleFactor = 0.5 + button.accessibilityIdentifier = "editor-menu-button" + button.accessibilityLabel = "More options" + button.accessibilityHint = "Double tap to see options" + button.addTarget(self, action: #selector(moreButtonPressed(sender:)), for: .touchUpInside) + button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + button.sizeToFit() + return button + }() + + + @objc func undoButtonPressed(sender: UIBarButtonItem) { + self.onUndoPressed() + } + + @objc func redoButtonPressed(sender: UIBarButtonItem) { + self.onRedoPressed() + } func registerLongPressGestureRecognizer() { longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) @@ -272,6 +327,24 @@ extension GutenbergViewController: GutenbergBridgeDelegate { func gutenbergDidRequestSendEventToHost(_ eventName: String, properties: [AnyHashable: Any]) -> Void { print("Gutenberg requested sending '\(eventName)' event to host with propreties: \(properties).") } + + func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) -> Void { + DispatchQueue.main.async { + UIView.animate(withDuration: 0.2) { + self.undoButton.isUserInteractionEnabled = isDisabled ? false : true + self.undoButton.alpha = isDisabled ? 0.3 : 1.0 + } + } + } + + func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) -> Void { + DispatchQueue.main.async { + UIView.animate(withDuration: 0.2) { + self.redoButton.isUserInteractionEnabled = isDisabled ? false : true + self.redoButton.alpha = isDisabled ? 0.3 : 1.0 + } + } + } } extension GutenbergViewController: GutenbergWebDelegate { @@ -362,7 +435,22 @@ extension GutenbergViewController { func configureNavigationBar() { addSaveButton() - addMoreButton() + addRightButtons() + + navigationController?.navigationBar.tintColor = UIColor(named: "Primary") + + // Add a bottom border to the navigation bar + let borderBottom = UIView() + borderBottom.backgroundColor = UIColor(named: "HeaderLine") + borderBottom.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] + borderBottom.frame = CGRect( + x: 0, + y: navigationController?.navigationBar.frame.height ?? 0 - (1.0 / UIScreen.main.scale), + width: navigationController?.navigationBar.frame.width ?? 0, + height: 1.0 / UIScreen.main.scale + ) + + navigationController?.navigationBar.addSubview(borderBottom) } func addSaveButton() { @@ -371,11 +459,12 @@ extension GutenbergViewController { action: #selector(saveButtonPressed(sender:))) } - func addMoreButton() { - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "...", - style: .plain, - target: self, - action: #selector(moreButtonPressed(sender:))) + func addRightButtons() { + let undoButton = UIBarButtonItem(customView: self.undoButton) + let redoButton = UIBarButtonItem(customView: self.redoButton) + let moreButton = UIBarButtonItem(customView: self.moreButton) + + navigationItem.rightBarButtonItems = [moreButton, redoButton, undoButton] } } @@ -459,6 +548,10 @@ extension GutenbergViewController { } func toggleHTMLMode(_ action: UIAlertAction) { + if !htmlMode { + self.gutenbergDidRequestToggleUndoButton(true) + self.gutenbergDidRequestToggleRedoButton(true) + } htmlMode = !htmlMode gutenberg.toggleHTMLMode() } @@ -466,4 +559,12 @@ extension GutenbergViewController { func showEditorHelp() { gutenberg.showEditorHelp() } + + func onUndoPressed() { + gutenberg.onUndoPressed() + } + + func onRedoPressed() { + gutenberg.onRedoPressed() + } } diff --git a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/Contents.json b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/Contents.json index 9a38aea4a8b79..d458f1c5928f7 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/Contents.json +++ b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info": { - "version": 1, - "author": "xcode" + "author": "xcode", + "version": 1 } } diff --git a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/Contents.json b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/Contents.json new file mode 100644 index 0000000000000..a7ab9ab17cc08 --- /dev/null +++ b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "filename": "editor-more.png", + "idiom": "universal", + "scale": "1x" + }, + { + "filename": "editor-more@2x.png", + "idiom": "universal", + "scale": "2x" + }, + { + "filename": "editor-more@3x.png", + "idiom": "universal", + "scale": "3x" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/editor-more.png b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/editor-more.png new file mode 100644 index 0000000000000000000000000000000000000000..50ae464cf9a0bdf62b6f483211d36b9dad604d10 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1RZkbkkcwN$2@7My zg4t;-tb*3L>o$DYUn1|+<-x%4AlY?g$kyMEq4C0D_NVqv2&*qw_~x|vm+NeQ?%m7` b4fQ-$cY;35>`^=hG?Ky7)z4*}Q$iB}Iese8 literal 0 HcmV?d00001 diff --git a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/editor-more@3x.png b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/more.imageset/editor-more@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..933dee6311d13524408be54adc0ec7c6c37b95ae GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawYCK&WLn>~)y>gJZL4n8ZVLgjm zl8W04x1IwT6&EBHv1cW9s=a^w>yme{3Q#pjgICx@P5sPelV<(;xGE^)anO0g*jU{y zg}STbi`gJ~sbKiUyylHWMAK&(gPP5s&ji%)FBkvMJI6Y?|IY6_+S%{)%pU^nWbkzL Kb6Mw<&;$S)-#jt^ literal 0 HcmV?d00001 diff --git a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/redo@2x.png b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/redo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7fcc8387ad5d98c9ba689c9b7cfa6c773b6d67af GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D9iA?ZAr-gYPIcrxY{26x-{Q#8 z;>6Knm?2TZ6Qw5EVyFS)I(^W4SNToF`|^|N*?ScDiJ*KE_unzCrcqm-^7pE;{E?!6A5m07BL`Gba-?ezSaqJO&!+7m9mwdN1* zDip8nRCDj2oZ#bN~yWDp#cR4CvYvTAZU~B!F$5j>g-^)5yNY=BI|GDaX l*T%0y^h0wK&~E!DylyP387ej0-T?i;;OXk;vd$@?2>`Q4UC{sl literal 0 HcmV?d00001 diff --git a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/redo@3x.png b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/redo.imageset/redo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..4bba41d829873127c99e33ed957c2b49d43b2a3e GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhauJY5_^DsH{K;Vsx~Akz9!nUQm1 z1B=Q72AM5YI|c#4%=Sd+5I{Fe9e~1tqnn5noFkyt-89n`^e0I zjmI9|2?%^Vp>1+-;KvTfs7DjcoOii1l-}UY>~iqZXfrvYAj?~_pnuyvt{|36GW}B? zPkgg(OR?V$K4qh8rympZp8?b;5Zn$@h{|bfWK_n6r0V%sfZwcIGu~#tomN zZW_;&+|AybT;6ZV#Qru=l}&x8yvtLD#h=xjpWToYY5KfV_`RV;hWVrA!Y+>%%uVQi zt-ZL=Au`#8k3-(_>-sjA8%q^0)Yo%7Fa8n$cDmQ!SFE*f8qH3wzibQ)3IU=hTK4j|Knv&a~{SuE}qdX8ZBgRX1Heg6*c^y3*>@MLDa#H5t9! z5`L9KKu2S#i}0D57cOR8Y}$2K%B{b+;>44l#of$zJl@Tj#L9Q?qOV$kF2PVT;-;OZ&-7<^I(4-s_i%W|TQzD8nFlF{r-XY2w;1 zMK466 literal 0 HcmV?d00001 diff --git a/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/undo.imageset/undo@3x.png b/packages/react-native-editor/ios/GutenbergDemo/Images.xcassets/undo.imageset/undo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..15703e1b6d63673b4e19616d7f03a6c57e9d7c47 GIT binary patch literal 310 zcmV-60m=S}P)A7?!WK29d1M)i}hfas|5u#A?*s9J3FP)z{3EkZ~4$rES5$^wb;z4KT=scu4 z3zyE5r!SXMcT9abV?}I?8Np&P$IfFB@oF<&O?67r1Je$b80?f1wx&oYp>!>AHV3N5 zaw-mrGyeFVo1 { + let toggleUndo; + let toggleRedo; + + beforeAll( () => { + subscribeOnUndoPressed.mockImplementation( ( callback ) => { + toggleUndo = callback; + } ); + subscribeOnRedoPressed.mockImplementation( ( callback ) => { + toggleRedo = callback; + } ); + } ); + it( 'should remove and add blocks', async () => { // Arrange const screen = await initializeEditor(); @@ -42,17 +62,17 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Undo' ) ); - fireEvent.press( screen.getByLabelText( 'Undo' ) ); - fireEvent.press( screen.getByLabelText( 'Undo' ) ); + toggleUndo(); + toggleUndo(); + toggleUndo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( `""` ); // Act - fireEvent.press( screen.getByLabelText( 'Redo' ) ); - fireEvent.press( screen.getByLabelText( 'Redo' ) ); - fireEvent.press( screen.getByLabelText( 'Redo' ) ); + toggleRedo(); + toggleRedo(); + toggleRedo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` @@ -95,7 +115,7 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Undo' ) ); + toggleUndo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` @@ -105,7 +125,7 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Redo' ) ); + toggleRedo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` @@ -145,7 +165,7 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Undo' ) ); + toggleUndo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` @@ -155,7 +175,7 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Undo' ) ); + toggleUndo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` @@ -165,8 +185,8 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Redo' ) ); - fireEvent.press( screen.getByLabelText( 'Redo' ) ); + toggleRedo(); + toggleRedo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` @@ -212,8 +232,8 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Undo' ) ); - fireEvent.press( screen.getByLabelText( 'Undo' ) ); + toggleUndo(); + toggleUndo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` @@ -223,8 +243,8 @@ describe( 'Editor History', () => { ` ); // Act - fireEvent.press( screen.getByLabelText( 'Redo' ) ); - fireEvent.press( screen.getByLabelText( 'Redo' ) ); + toggleRedo(); + toggleRedo(); // Assert expect( getEditorHtml() ).toMatchInlineSnapshot( ` diff --git a/test/native/setup.js b/test/native/setup.js index 817eb93e0c439..ece2e289e6c09 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -104,6 +104,8 @@ jest.mock( '@wordpress/react-native-bridge', () => { subscribeShowNotice: jest.fn(), subscribeParentGetHtml: jest.fn(), subscribeShowEditorHelp: jest.fn(), + subscribeOnUndoPressed: jest.fn(), + subscribeOnRedoPressed: jest.fn(), editorDidMount: jest.fn(), editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), @@ -124,6 +126,8 @@ jest.mock( '@wordpress/react-native-bridge', () => { fetchRequest: jest.fn(), requestPreview: jest.fn(), generateHapticFeedback: jest.fn(), + toggleUndoButton: jest.fn(), + toggleRedoButton: jest.fn(), }; } );