From 030a3d00658ab40076a86dcc99df7c4213813774 Mon Sep 17 00:00:00 2001 From: Ward Abbass Date: Tue, 9 Nov 2021 12:21:22 +0200 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 131d6baaaaf650f3bb786f628e7b898954d3f467 Author: Danilo Bürger Date: Mon Nov 8 22:13:38 2021 +0100 Fix default font handling on android (#7333) * Pass null font family into typeface loader if not set ReactTypefaceUtils handles null font families to fall back to the default typeface * Provide the default typeface when calling typeface loader * Feed paint typeface as default typeface into typeface loader to support default fonts * Support default font face via AHTextView in bottom tabs * Move defaultTypeFace to typeFace loader Co-authored-by: Ward Abbass commit 5136a2e8328d8147754d954b7551d35da7549e51 Author: Ward Abbass Date: Mon Nov 8 15:22:57 2021 +0200 React Native 66 (#7305) * RN 65 React Native upgrade + babel unit tests running as expected * RN66 - Normal unit tests working - Upgrade to RN66. - Move jest to jest config file. - Use babel-test transformer to make unit tests working - e2e tests that can run as unit wont work. * disable running e2es as units * use Hermes + Fix Reanimated * update reanimated and use babel plugin * Enable coverage * Upgrade jest and babel to 27 * enable JS e2e add animatable to transform ignore * use babel jest as transformer instead of the react native one * update android to 30 * Fix overlay insets and use non deprecated methods * Kotlin convert * tmp status * Update Detox to support iOS 15 * Fix e2e * Revert "tmp status" This reverts commit aff2b87bf8837f18abdf1164e415342c1376c04a. * prevent ReactRootViews from having ids * status bar utils static fields * Squashed commit of the following: commit 59596385b86af1b39417c14425af488b09250bf6 Author: Ward Abbass Date: Wed Oct 27 13:04:00 2021 +0300 Keyboard demo playground (#7331) Adding a simple demo that can be used to demonstrate Keyboard show/dismiss when showing/dismissing modal when clicking on submit Co-authored-by: Yogev Ben David * wait for stack/modal to be shown to send didAppear this happens since RN 64 * Fix tests * Ensure modal is not already presented * use 0.66.2 Co-authored-by: svbutko Co-authored-by: Yogev Ben David commit 964752000d32f0273f1c56d63db9ae379a487276 Author: Ward Abbass Date: Mon Nov 8 15:05:00 2021 +0200 Android context crash when parsing options (#7342) # Issue: When parsing options, `ReactInstanceManager.getCurrentContext()` might return null, which caused color parsing to throw null exception since it assumes that context is not null. # Diagnose: This can be the case when Activity was killed but the Application still alive (Don't keep activity for example), and since js is bound to the Application lifecycle, opening the activity triggers recreating context in bg, in the meantime calling a command that requires reactContext can cause such crashes to unwanted behaviours when react context is needed. This happens internally and it looks like it is some flow where calling navigation command when react is not ready. # Fix: Since the issue did not happen before, and the crash occurred in parsing options that are independent of `ReactContext`, we can use Activity or Application as the context for such a case. commit f35d410d371260423a3e100038ad943b204a916c Author: Yogev Ben David Date: Sun Nov 7 16:59:59 2021 +0200 Remove manually dismissing all modals on setRoot (#7340) Currently we manually dismiss all modals on `setRoot`, we should instead clear it from the modal manager so that those controllers will be automatically released by the reference count system. commit 7347ed6fa71781c7646b6aec45122a50f9b5aaac Author: Yogev Ben David Date: Thu Oct 28 12:50:23 2021 +0300 Fix missing props after setRoot for components with identical predefined id (#7329) * Destroy buttons along with the main react view * Revert "Destroy buttons along with the main react view" This reverts commit 27e604db1686a12bfec737a657ad5c033857a887. * Add pending props * Fix android button components cache Co-authored-by: Ward Abbass Co-authored-by: Ward Abbass commit 340146747281e7a5d52042596ae5192af992f494 Author: Ward Abbass Date: Wed Oct 27 18:00:22 2021 +0300 MergeOptions, buttons got cleared when animation enabled [Android] (#7330) # Issue: When having `animateLeftButtons` or `animateRightButtons` enabled, calling `mergeOptions` frequently to update button state like enabled, colour and text etc, caused the menu animation to mess out the views inside the menu. # Diagnoses: - Calling `mergeOptions` inside `componentDidUpdate`, which can be called frequently without any options changes deducted from props like calling setState for example or changing a prop that does not have something to do the merge options call. - Every `mergeOptions` with buttons in it, would remove and re-add buttons to the toolbars which caused the animation to run, even if the change was updating button colour or enabled state. # Fix: - Introduce a more efficient way to detect updates and update each button in case there are no structural changes. - Structural changes can be: reordering, removing, adding buttons or changing buttons with the same `button.id` but a different component with no stable `componentId`. - Other changes mentioned above will call update buttons in place with no remove and re-add and no animation. - Refactor the code and enhance single responsibilities for each controller. - Added more test cases for such flows. Note: Android built-in menu, has its limits in terms of updating and rearranging action views inside it, it would be possible if we built a custom menu with an overflow that can support, order changes, and in-place updates, and that is a long road to walk. Co-authored-by: Yogev Ben David commit 59596385b86af1b39417c14425af488b09250bf6 Author: Ward Abbass Date: Wed Oct 27 13:04:00 2021 +0300 Keyboard demo playground (#7331) Adding a simple demo that can be used to demonstrate Keyboard show/dismiss when showing/dismissing modal when clicking on submit Co-authored-by: Yogev Ben David --- e2e/Keyboard.test.js | 28 ++ e2e/SetRoot.test.js | 14 +- .../options/FontOptions.kt | 4 +- .../options/LayoutFactory.java | 49 ++-- .../options/Options.java | 2 +- .../options/parsers/TypefaceLoader.kt | 26 +- .../react/modal/ModalFrameLayout.kt | 2 +- .../utils/StatusBarUtils.kt | 1 + .../bottomtabs/BottomTabPresenter.java | 7 +- .../viewcontrollers/modal/ModalPresenter.java | 10 +- .../stack/StackController.java | 19 +- .../viewcontrollers/stack/StackPresenter.java | 220 +++++++-------- .../stack/topbar/TopBarController.kt | 205 ++++++++++++-- .../stack/topbar/button/ButtonController.kt | 29 +- .../stack/topbar/button/ButtonPresenter.kt | 10 +- .../stack/topbar/button/ButtonSpan.kt | 2 +- .../views/stack/topbar/titlebar/ButtonBar.kt | 19 +- .../titlebar/TitleAndButtonsContainer.kt | 2 - .../topbar/titlebar/TitleSubTitleLayout.kt | 7 - .../utils/LayoutFactoryTest.java | 27 +- ...urer.kt => TitleAndButtonsMeasurerTest.kt} | 4 +- .../modal/ModalPresenterTest.java | 4 + .../viewcontrollers/modal/ModalStackTest.java | 14 +- .../navigator/NavigatorTest.java | 4 +- .../stack/StackControllerTest.kt | 15 +- .../stack/StackPresenterTest.kt | 101 +++---- .../stack/TopBarControllerTest.kt | 257 +++++++++++++++--- lib/ios/RNNAssert.h | 4 +- lib/ios/RNNBottomTabOptions.m | 3 +- lib/ios/RNNButtonBuilder.m | 6 +- lib/ios/RNNButtonOptions.m | 8 +- lib/ios/RNNCommandsHandler.m | 5 +- lib/ios/RNNDotIndicatorPresenter.m | 3 +- lib/ios/RNNModalManager.h | 2 + lib/ios/RNNModalManager.m | 19 +- lib/ios/RNNSegmentedControl.h | 2 +- lib/ios/RNNTabBarItemCreator.m | 5 +- lib/ios/RNNUIBarButtonItem.h | 2 +- lib/ios/RNNUIBarButtonItem.m | 6 +- lib/ios/TopBarPresenter.m | 12 +- lib/src/commands/Commands.test.ts | 10 +- lib/src/commands/LayoutTreeCrawler.test.ts | 2 +- lib/src/commands/LayoutTreeCrawler.ts | 2 +- lib/src/commands/OptionsProcessor.test.ts | 4 +- lib/src/commands/OptionsProcessor.ts | 4 +- lib/src/components/Store.test.ts | 16 ++ lib/src/components/Store.ts | 9 + package.json | 2 +- playground/src/app.ts | 5 + playground/src/screens/KeyboardScreen.tsx | 111 +++++++- playground/src/screens/LayoutsScreen.tsx | 5 + playground/src/screens/ModalScreen.tsx | 39 +-- playground/src/screens/Screens.ts | 1 + playground/src/screens/SetRootScreen.tsx | 60 ++++ playground/src/screens/index.tsx | 5 +- playground/src/testIDs.ts | 5 + 56 files changed, 1043 insertions(+), 396 deletions(-) create mode 100644 e2e/Keyboard.test.js rename lib/android/app/src/test/java/com/reactnativenavigation/utils/{TitleAndButtonsMeasurer.kt => TitleAndButtonsMeasurerTest.kt} (99%) diff --git a/e2e/Keyboard.test.js b/e2e/Keyboard.test.js new file mode 100644 index 00000000000..18420fc46ab --- /dev/null +++ b/e2e/Keyboard.test.js @@ -0,0 +1,28 @@ +import { default as TestIDs, default as testIDs } from '../playground/src/testIDs'; +import Android from './AndroidUtils'; +import Utils from './Utils'; + +const { elementByLabel, elementById } = Utils; + +describe('Keyboard', () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + await elementById(TestIDs.KEYBOARD_SCREEN_BTN).tap(); + }); + + it('Push - should close keyboard when Back clicked', async () => { + await elementById(TestIDs.TEXT_INPUT1).tap(); + await expect(elementByLabel("Keyboard Demo")).not.toBeVisible(); + await elementById(TestIDs.BACK_BUTTON).tap(); + await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible(); + }); + + it('Modal - should close keyboard when close clicked', async () => { + await elementById(TestIDs.MODAL_BTN).tap(); + await elementById(TestIDs.TEXT_INPUT1).tap(); + await expect(elementByLabel("Keyboard Demo")).not.toBeVisible(); + await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap(); + await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible(); + }); + +}); diff --git a/e2e/SetRoot.test.js b/e2e/SetRoot.test.js index d1547e9716d..4a7f7154c70 100644 --- a/e2e/SetRoot.test.js +++ b/e2e/SetRoot.test.js @@ -1,7 +1,7 @@ import Utils from './Utils'; import TestIDs from '../playground/src/testIDs'; -const { elementById } = Utils; +const { elementById, elementByLabel } = Utils; describe('SetRoot', () => { beforeEach(async () => { @@ -33,4 +33,16 @@ describe('SetRoot', () => { await elementById(TestIDs.SET_ROOT_WITHOUT_STACK_HIDES_BOTTOM_TABS_BTN).tap(); await expect(elementById(TestIDs.LAYOUTS_TAB)).toBeNotVisible(); }); + + it('set root should not override props for component with identical id', async () => { + await expect(elementByLabel('Two')).toBeVisible(); + await elementById(TestIDs.ROUND_BUTTON).tap(); + await expect(elementByLabel('Times created: 1')).toBeVisible(); + await elementById(TestIDs.OK_BUTTON).tap(); + await elementById(TestIDs.SET_ROOT_WITH_BUTTONS).tap(); + await expect(elementByLabel('Two')).toBeVisible(); + await elementById(TestIDs.ROUND_BUTTON).tap(); + await expect(elementByLabel('Times created: 1')).toBeVisible(); + await elementById(TestIDs.OK_BUTTON).tap(); + }); }); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/options/FontOptions.kt b/lib/android/app/src/main/java/com/reactnativenavigation/options/FontOptions.kt index bdb2255c886..c97c32a6759 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/options/FontOptions.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/options/FontOptions.kt @@ -26,11 +26,11 @@ class FontOptions { @JvmOverloads fun getTypeface(typefaceLoader: TypefaceLoader, defaultTypeface: Typeface? = null): Typeface? { if (isDirty) { - _typeface = typefaceLoader.getTypeFace(fontFamily.get(""), fontStyle.get(""), fontWeight.get("")) + _typeface = typefaceLoader.getTypeFace(fontFamily.get(null), fontStyle.get(""), fontWeight.get(""), defaultTypeface) isDirty = false } return _typeface - ?: defaultTypeface?.let { typefaceLoader.getTypeFace(fontFamily.get(""), fontStyle.get(""), fontWeight.get(""), it) } + ?: defaultTypeface?.let { typefaceLoader.getTypeFace(fontFamily.get(null), fontStyle.get(""), fontWeight.get(""), it) } } fun mergeWith(other: FontOptions) { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/options/LayoutFactory.java b/lib/android/app/src/main/java/com/reactnativenavigation/options/LayoutFactory.java index 7e3ff064736..8a667162040 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/options/LayoutFactory.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/options/LayoutFactory.java @@ -1,9 +1,11 @@ package com.reactnativenavigation.options; import android.app.Activity; +import android.content.Context; import com.facebook.react.ReactInstanceManager; import com.facebook.react.bridge.ReactContext; +import com.reactnativenavigation.NavigationApplication; import com.reactnativenavigation.options.parsers.TypefaceLoader; import com.reactnativenavigation.react.events.EventEmitter; import com.reactnativenavigation.utils.Assertions; @@ -45,6 +47,8 @@ import static com.reactnativenavigation.options.Options.parse; import static com.reactnativenavigation.utils.CollectionUtils.*; +import org.json.JSONObject; + public class LayoutFactory { private Activity activity; private ChildControllersRegistry childRegistry; @@ -75,15 +79,15 @@ public ViewController create(final LayoutNode node) { final ReactContext context = reactInstanceManager.getCurrentReactContext(); switch (node.type) { case Component: - return createComponent(context, node); + return createComponent(node); case ExternalComponent: return createExternalComponent(context, node); case Stack: - return createStack(context, node); + return createStack(node); case BottomTabs: - return createBottomTabs(context, node); + return createBottomTabs(node); case SideMenuRoot: - return createSideMenuRoot(context, node); + return createSideMenuRoot(node); case SideMenuCenter: return createSideMenuContent(node); case SideMenuLeft: @@ -91,17 +95,17 @@ public ViewController create(final LayoutNode node) { case SideMenuRight: return createSideMenuRight(node); case TopTabs: - return createTopTabs(context, node); + return createTopTabs(node); default: throw new IllegalArgumentException("Invalid node type: " + node.type); } } - private ViewController createSideMenuRoot(ReactContext context, LayoutNode node) { + private ViewController createSideMenuRoot(LayoutNode node) { SideMenuController sideMenuController = new SideMenuController(activity, childRegistry, node.id, - parse(context, typefaceManager, node.getOptions()), + parseOptions( node.getOptions()), new SideMenuPresenter(), new Presenter(activity, defaultOptions) ); @@ -153,7 +157,7 @@ private ViewController createSideMenuRight(LayoutNode node) { return create(node.children.get(0)); } - private ViewController createComponent(ReactContext context, LayoutNode node) { + private ViewController createComponent(LayoutNode node) { String id = node.id; String name = node.data.optString("name"); return new ComponentViewController(activity, @@ -161,7 +165,7 @@ private ViewController createComponent(ReactContext context, LayoutNode node) id, name, new ComponentViewCreator(reactInstanceManager), - parse(context, typefaceManager, node.getOptions()), + parseOptions(node.getOptions()), new Presenter(activity, defaultOptions), new ComponentPresenter(defaultOptions) ); @@ -178,17 +182,17 @@ private ViewController createExternalComponent(ReactContext context, LayoutNo reactInstanceManager, new EventEmitter(context), new ExternalComponentPresenter(), - parse(context, typefaceManager, node.getOptions()) + parseOptions(node.getOptions()) ); } - private ViewController createStack(ReactContext context, LayoutNode node) { + private ViewController createStack(LayoutNode node) { return new StackControllerBuilder(activity, eventEmitter) .setChildren(createChildren(node.children)) .setChildRegistry(childRegistry) .setTopBarController(new TopBarController()) .setId(node.id) - .setInitialOptions(parse(context, typefaceManager, node.getOptions())) + .setInitialOptions(parseOptions(node.getOptions())) .setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreator(reactInstanceManager), new TopBarBackgroundViewCreator(reactInstanceManager), @@ -210,7 +214,7 @@ private List> createChildren(List children) { return result; } - private ViewController createBottomTabs(ReactContext context, LayoutNode node) { + private ViewController createBottomTabs(LayoutNode node) { List> tabs = map(node.children, this::create); BottomTabsPresenter bottomTabsPresenter = new BottomTabsPresenter(tabs, defaultOptions, new BottomTabsAnimator()); return new BottomTabsController(activity, @@ -219,24 +223,35 @@ private ViewController createBottomTabs(ReactContext context, LayoutNode node eventEmitter, new ImageLoader(), node.id, - parse(context, typefaceManager, node.getOptions()), + parseOptions( node.getOptions()), new Presenter(activity, defaultOptions), new BottomTabsAttacher(tabs, bottomTabsPresenter, defaultOptions), bottomTabsPresenter, new BottomTabPresenter(activity, tabs, new ImageLoader(), new TypefaceLoader(activity), defaultOptions)); } - private ViewController createTopTabs(ReactContext context, LayoutNode node) { + private ViewController createTopTabs(LayoutNode node) { final List> tabs = new ArrayList<>(); for (int i = 0; i < node.children.size(); i++) { ViewController tabController = create(node.children.get(i)); - Options options = parse(context, typefaceManager, node.children.get(i).getOptions()); + Options options = parseOptions(node.children.get(i).getOptions()); options.setTopTabIndex(i); tabs.add(tabController); } - return new TopTabsController(activity, childRegistry, node.id, tabs, new TopTabsLayoutCreator(activity, tabs), parse(context, typefaceManager, node.getOptions()), new Presenter(activity, defaultOptions)); + return new TopTabsController(activity, childRegistry, node.id, tabs, new TopTabsLayoutCreator(activity, tabs) + , parseOptions(node.getOptions()), new Presenter(activity, defaultOptions)); } + private Options parseOptions(JSONObject jsonOptions) { + Context context = reactInstanceManager.getCurrentReactContext(); + if (context == null) { + context = activity == null ? NavigationApplication.instance : activity; + } + if (typefaceManager == null) { + typefaceManager = new TypefaceLoader(context); + } + return parse(context, typefaceManager, jsonOptions); + } @NonNull @RestrictTo(RestrictTo.Scope.TESTS) public Options getDefaultOptions() { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/options/Options.java b/lib/android/app/src/main/java/com/reactnativenavigation/options/Options.java index 262086ed3fe..a169c67c0e8 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/options/Options.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/options/Options.java @@ -15,7 +15,7 @@ public class Options { public static final Options EMPTY = new Options(); @NonNull - public static Options parse(Context context, TypefaceLoader typefaceManager, JSONObject json) { + public static Options parse(@NonNull Context context, TypefaceLoader typefaceManager, JSONObject json) { Options result = new Options(); if (json == null) return result; diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/options/parsers/TypefaceLoader.kt b/lib/android/app/src/main/java/com/reactnativenavigation/options/parsers/TypefaceLoader.kt index c1f8fb6aa74..3ddd4a5fc23 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/options/parsers/TypefaceLoader.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/options/parsers/TypefaceLoader.kt @@ -2,21 +2,27 @@ package com.reactnativenavigation.options.parsers import android.content.Context import android.graphics.Typeface +import com.aurelhubert.ahbottomnavigation.AHTextView import com.reactnativenavigation.utils.ReactTypefaceUtils open class TypefaceLoader(private val context: Context) { - @JvmOverloads open fun getTypeFace( - fontFamilyName: String?, - fontStyle: String?, - fontWeight: String?, - defaultTypeFace: Typeface? = null + val defaultTypeFace: Typeface by lazy { + AHTextView(context).typeface ?: Typeface.DEFAULT + } + + @JvmOverloads + open fun getTypeFace( + fontFamilyName: String?, + fontStyle: String?, + fontWeight: String?, + defaultTypeFace: Typeface? = null ): Typeface? { return ReactTypefaceUtils.applyStyles( - defaultTypeFace, - ReactTypefaceUtils.parseFontStyle(fontStyle), - ReactTypefaceUtils.parseFontWeight(fontWeight), - fontFamilyName, - context.assets + defaultTypeFace, + ReactTypefaceUtils.parseFontStyle(fontStyle), + ReactTypefaceUtils.parseFontWeight(fontWeight), + fontFamilyName, + context.assets ) } } \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalFrameLayout.kt b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalFrameLayout.kt index 4ae7901ade3..636b025336c 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalFrameLayout.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalFrameLayout.kt @@ -11,7 +11,7 @@ class ModalFrameLayout(context: ReactContext) : FrameLayout(context) { addView(modalContentLayout, MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT) .apply { val translucent = context.currentActivity?.window?.let { - StatusBarUtils.isTranslucent(context.currentActivity?.window) + StatusBarUtils.isTranslucent(it) } ?: false topMargin = if (translucent) 0 else StatusBarUtils.getStatusBarHeight(context.currentActivity) }) diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/utils/StatusBarUtils.kt b/lib/android/app/src/main/java/com/reactnativenavigation/utils/StatusBarUtils.kt index 915bb16d956..1bcb0cfdc3b 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/utils/StatusBarUtils.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/utils/StatusBarUtils.kt @@ -11,6 +11,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import kotlin.math.abs + object StatusBarUtils { private const val STATUS_BAR_HEIGHT_M = 24 private const val STATUS_BAR_HEIGHT_L = 25 diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java index a7da579569a..0a49cf669a4 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; +import com.aurelhubert.ahbottomnavigation.AHTextView; import com.aurelhubert.ahbottomnavigation.notification.AHNotification; import com.reactnativenavigation.options.BottomTabOptions; import com.reactnativenavigation.options.DotIndicatorOptions; @@ -27,6 +28,7 @@ public class BottomTabPresenter { private final Context context; private final ImageLoader imageLoader; private final TypefaceLoader typefaceLoader; + private final Typeface defaultTypeface; private Options defaultOptions; private final BottomTabFinder bottomTabFinder; private final LateInit bottomTabs = new LateInit<>(); @@ -39,6 +41,7 @@ public BottomTabPresenter(Context context, List> tabs, ImageLo this.bottomTabFinder = new BottomTabFinder(tabs); this.imageLoader = imageLoader; this.typefaceLoader = typefaceLoader; + this.defaultTypeface = typefaceLoader.getDefaultTypeFace(); this.defaultOptions = defaultOptions; defaultDotIndicatorSize = dpToPx(context, 6); } @@ -57,7 +60,7 @@ public void applyOptions() { BottomTabOptions tab = tabs.get(i).resolveCurrentOptions(defaultOptions).bottomTabOptions; bottomTabs.setIconWidth(i, tab.iconWidth.get(null)); bottomTabs.setIconHeight(i, tab.iconHeight.get(null)); - bottomTabs.setTitleTypeface(i, tab.font.getTypeface(typefaceLoader, Typeface.DEFAULT)); + bottomTabs.setTitleTypeface(i, tab.font.getTypeface(typefaceLoader, defaultTypeface)); if (tab.selectedIconColor.canApplyValue()) bottomTabs.setIconActiveColor(i, tab.selectedIconColor.get(null)); if (tab.iconColor.canApplyValue()) bottomTabs.setIconInactiveColor(i, tab.iconColor.get(null)); bottomTabs.setTitleActiveColor(i, tab.selectedTextColor.get(null)); @@ -86,7 +89,7 @@ public void mergeChildOptions(Options options, ViewController child) { BottomTabOptions tab = options.bottomTabOptions; if (tab.iconWidth.hasValue()) bottomTabs.setIconWidth(index, tab.iconWidth.get(null)); if (tab.iconHeight.hasValue()) bottomTabs.setIconHeight(index, tab.iconHeight.get(null)); - if (tab.font.hasValue()) bottomTabs.setTitleTypeface(index, tab.font.getTypeface(typefaceLoader, Typeface.DEFAULT)); + if (tab.font.hasValue()) bottomTabs.setTitleTypeface(index, tab.font.getTypeface(typefaceLoader, defaultTypeface)); if (canMergeColor(tab.selectedIconColor)) bottomTabs.setIconActiveColor(index, tab.selectedIconColor.get()); if (canMergeColor(tab.iconColor)) bottomTabs.setIconInactiveColor(index, tab.iconColor.get()); if (tab.selectedTextColor.hasValue()) bottomTabs.setTitleActiveColor(index, tab.selectedTextColor.get()); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java index 81382b1a589..3df53a45b45 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java @@ -90,10 +90,12 @@ public void onCancel() { } private void onShowModalEnd(ViewController toAdd, @Nullable ViewController toRemove, CommandListener listener) { - toAdd.onViewDidAppear(); - if (toRemove != null && toAdd.resolveCurrentOptions(defaultOptions).modal.presentationStyle != ModalPresentationStyle.OverCurrentContext) { - toRemove.detachView(); - } + toAdd.addOnAppearedListener(()->{ + toAdd.onViewDidAppear(); + if (toRemove != null && toAdd.resolveCurrentOptions(defaultOptions).modal.presentationStyle != ModalPresentationStyle.OverCurrentContext) { + toRemove.detachView(); + } + }); listener.onSuccess(toAdd.getId()); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java index 099d22e1c17..c2fcafafb80 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java @@ -69,7 +69,7 @@ public StackController(Activity activity, List> children, Chil @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - presenter.onConfigurationChanged(resolveCurrentOptions()); + presenter.onConfigurationChanged(resolveCurrentOptions(), getCurrentChild()); fabPresenter.onConfigurationChanged(resolveCurrentOptions()); } @@ -176,9 +176,7 @@ public void push(ViewController child, CommandListener listener) { presenter.getAdditionalPushAnimations(this, child, resolvedOptions), () -> onPushAnimationComplete(child, toRemove, listener)); } else { - child.onViewDidAppear(); - getView().removeView(toRemove.getView()); - listener.onSuccess(child.getId()); + onPushAnimationComplete(child, toRemove, listener); } } else { listener.onSuccess(child.getId()); @@ -192,8 +190,10 @@ public void destroy() { } private void onPushAnimationComplete(ViewController toAdd, ViewController toRemove, CommandListener listener) { - toAdd.onViewDidAppear(); - if (!peek().equals(toRemove)) getView().removeView(toRemove.getView()); + toAdd.addOnAppearedListener(() -> { + toAdd.onViewDidAppear(); + if (!peek().equals(toRemove)) getView().removeView(toRemove.getView()); + }); listener.onSuccess(toAdd.getId()); } @@ -429,11 +429,10 @@ private void addInitialChild(StackLayout stackLayout) { } private void setChildId(ViewGroup child) { - //In RN > 64 ReactRootView ids are managed by ReactNative - // see: https://github.com/facebook/react-native/blob/main/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java#L676 - if(!(child instanceof ReactRootView)){ + //From RN > 64 we can't set id to child that is ReactRootView + //see:https://github.com/facebook/react-native/blob/main/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java#L676 + if (!(child instanceof ReactRootView)) child.setId(CompatUtils.generateViewId()); - } } private void startChildrenBellowTopChild() { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenter.java index 313a9c3912c..4fc0950ef75 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenter.java @@ -1,9 +1,17 @@ package com.reactnativenavigation.viewcontrollers.stack; +import static com.reactnativenavigation.utils.CollectionUtils.filter; +import static com.reactnativenavigation.utils.CollectionUtils.forEach; +import static com.reactnativenavigation.utils.CollectionUtils.isNullOrEmpty; +import static com.reactnativenavigation.utils.CollectionUtils.merge; +import static com.reactnativenavigation.utils.ObjectUtils.perform; +import static com.reactnativenavigation.viewcontrollers.stack.topbar.TopBarControllerKt.DEFAULT_BORDER_COLOR; + import android.animation.Animator; import android.app.Activity; import android.graphics.Color; import android.view.View; +import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; import android.widget.FrameLayout; @@ -47,24 +55,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import static com.reactnativenavigation.utils.CollectionUtils.difference; -import static com.reactnativenavigation.utils.CollectionUtils.filter; -import static com.reactnativenavigation.utils.CollectionUtils.first; -import static com.reactnativenavigation.utils.CollectionUtils.forEach; -import static com.reactnativenavigation.utils.CollectionUtils.getOrDefault; -import static com.reactnativenavigation.utils.CollectionUtils.isNullOrEmpty; -import static com.reactnativenavigation.utils.CollectionUtils.keyBy; -import static com.reactnativenavigation.utils.CollectionUtils.merge; -import static com.reactnativenavigation.utils.ObjectUtils.perform; -import static com.reactnativenavigation.utils.ObjectUtils.take; - public class StackPresenter { - private static final int DEFAULT_BORDER_COLOR = Color.BLACK; private static final double DEFAULT_ELEVATION = 4d; private final Activity activity; @@ -79,12 +74,10 @@ public class StackPresenter { private final TitleBarButtonCreator buttonCreator; private Options defaultOptions; - private List currentRightButtons = new ArrayList<>(); - private List currentLeftButtons = new ArrayList<>(); private final Map titleControllers = new HashMap(); private final Map backgroundControllers = new HashMap(); - private final Map> componentRightButtons = new HashMap(); - private final Map> componentLeftButtons = new HashMap(); + private final Map> rightButtonControllers = new HashMap(); + private final Map> leftButtonControllers = new HashMap(); private final IconResolver iconResolver; private final TypefaceLoader typefaceLoader; @@ -126,8 +119,8 @@ public void bindView(TopBarController topBarController, @Nullable BottomTabsCont public boolean isRendered(View component) { ArrayList> controllers = new ArrayList<>(); - controllers.addAll(perform(componentRightButtons.get(component), new ArrayList<>(), Map::values)); - controllers.addAll(perform(componentLeftButtons.get(component), new ArrayList<>(), Map::values)); + controllers.addAll(perform(rightButtonControllers.get(component), new ArrayList<>(), Map::values)); + controllers.addAll(perform(leftButtonControllers.get(component), new ArrayList<>(), Map::values)); controllers.add(backgroundControllers.get(component)); controllers.add(titleControllers.get(component)); return renderChecker.areRendered(filter(controllers, ObjectUtils::notNull)); @@ -136,29 +129,21 @@ public boolean isRendered(View component) { public void mergeOptions(Options options, StackController stack, ViewController currentChild) { TopBarOptions resolvedTopBarOptions = options.topBar.copy().mergeWithDefault(stack.resolveChildOptions(currentChild).topBar).mergeWithDefault(defaultOptions.topBar); mergeOrientation(options.layout.orientation); - // mergeButtons(topBar, withDefault.topBar.buttons, child); mergeTopBarOptions(resolvedTopBarOptions, options, stack, currentChild); mergeTopTabsOptions(options.topTabs); mergeTopTabOptions(options.topTabOptions); } - public void onConfigurationChanged(Options options) { + public void onConfigurationChanged(Options options, ViewController currentChild) { if (topBar == null) return; Options withDefault = options.copy().withDefaultOptions(defaultOptions); - if (currentRightButtons != null && !currentRightButtons.isEmpty()) - topBarController.applyRightButtons(currentRightButtons); - if (currentLeftButtons != null && !currentLeftButtons.isEmpty()) - topBarController.applyLeftButtons(currentLeftButtons); if (withDefault.topBar.buttons.back.visible.isTrue()) { - topBar.setBackButton(createButtonController(withDefault.topBar.buttons.back)); + topBarController.setBackButton(createButtonController(withDefault.topBar.buttons.back)); } - topBar.setOverflowButtonColor(withDefault.topBar.rightButtonColor.get(Color.BLACK)); - topBar.applyTopTabsColors(withDefault.topTabs.selectedTabColor, - withDefault.topTabs.unselectedTabColor); - topBar.setBorderColor(withDefault.topBar.borderColor.get(DEFAULT_BORDER_COLOR)); - topBar.setBackgroundColor(withDefault.topBar.background.color.get(Color.WHITE)); - topBar.setTitleTextColor(withDefault.topBar.title.color.get(TopBar.DEFAULT_TITLE_COLOR)); - topBar.setSubtitleColor(withDefault.topBar.subtitle.color.get(TopBar.DEFAULT_TITLE_COLOR)); + topBarController.onConfigurationChanged(withDefault, + leftButtonControllers.get(currentChild.getView()), + rightButtonControllers.get(currentChild.getView())); + } public void applyInitialChildLayoutOptions(Options options) { @@ -183,10 +168,10 @@ public void applyOrientation(OrientationOptions options) { public void onChildDestroyed(ViewController child) { perform(titleControllers.remove(child.getView()), TitleBarReactViewController::destroy); perform(backgroundControllers.remove(child.getView()), TopBarBackgroundViewController::destroy); - destroyButtons(componentRightButtons.get(child.getView())); - destroyButtons(componentLeftButtons.get(child.getView())); - componentRightButtons.remove(child.getView()); - componentLeftButtons.remove(child.getView()); + destroyButtons(rightButtonControllers.get(child.getView())); + destroyButtons(leftButtonControllers.get(child.getView())); + rightButtonControllers.remove(child.getView()); + leftButtonControllers.remove(child.getView()); } private void destroyButtons(@Nullable Map buttons) { @@ -258,7 +243,7 @@ private void applyTopBarOptions(Options options, StackController stack, ViewCont } private void applyStatusBarDrawBehindOptions(TopBarOptions topBarOptions, Options withDefault) { - if(withDefault.statusBar.visible.isTrueOrUndefined() && withDefault.statusBar.drawBehind.isTrue()){ + if (withDefault.statusBar.visible.isTrueOrUndefined() && withDefault.statusBar.drawBehind.isTrue()) { topBar.setTopPadding(StatusBarUtils.getStatusBarHeight(activity)); topBar.setHeight(topBarOptions.height.get(UiUtils.getTopBarHeightDp(activity)) + StatusBarUtils.getStatusBarHeightDp(activity)); } else { @@ -268,8 +253,8 @@ private void applyStatusBarDrawBehindOptions(TopBarOptions topBarOptions, Option } private void mergeStatusBarDrawBehindOptions(TopBarOptions topBarOptions, Options toMerge) { - if(toMerge.statusBar.drawBehind.hasValue()){ - if(toMerge.statusBar.visible.isTrueOrUndefined() && toMerge.statusBar.drawBehind.isTrue()){ + if (toMerge.statusBar.drawBehind.hasValue()) { + if (toMerge.statusBar.visible.isTrueOrUndefined() && toMerge.statusBar.drawBehind.isTrue()) { topBar.setTopPadding(StatusBarUtils.getStatusBarHeight(activity)); topBar.setHeight(topBarOptions.height.get(UiUtils.getTopBarHeightDp(activity)) + StatusBarUtils.getStatusBarHeightDp(activity)); } else { @@ -304,59 +289,21 @@ private void applyTopBarVisibility(TopBarOptions options) { } private void applyButtons(TopBarOptions options, ViewController child) { - if (options.buttons.right != null) { - List rightButtons = mergeButtonsWithColor(options.buttons.right, - options.rightButtonColor - , options.rightButtonDisabledColor); - List rightButtonControllers = getOrCreateButtonControllersByInstanceId(componentRightButtons.get(child.getView()), rightButtons); - componentRightButtons.put(child.getView(), keyBy(rightButtonControllers, ButtonController::getButtonInstanceId)); - if (!CollectionUtils.equals(currentRightButtons, rightButtonControllers)) { - currentRightButtons = rightButtonControllers; - topBarController.applyRightButtons(currentRightButtons); - } - } else { - currentRightButtons = null; - topBar.clearRightButtons(); - } - - if (options.buttons.left != null) { - List leftButtons = mergeButtonsWithColor(options.buttons.left, - options.leftButtonColor, - options.leftButtonDisabledColor); - List leftButtonControllers = getOrCreateButtonControllersByInstanceId(componentLeftButtons.get(child.getView()), leftButtons); - componentLeftButtons.put(child.getView(), keyBy(leftButtonControllers, ButtonController::getButtonInstanceId)); - if (!CollectionUtils.equals(currentLeftButtons, leftButtonControllers)) { - currentLeftButtons = leftButtonControllers; - topBarController.applyLeftButtons(currentLeftButtons); - } - } else { - currentLeftButtons = null; - topBar.clearLeftButtons(); - } - - if (options.buttons.back.visible.isTrue() && !options.buttons.hasLeftButtons()) { - topBar.setBackButton(createButtonController(options.buttons.back)); - } + //should be at first in order for next actions to be animated if (options.animateRightButtons.hasValue()) - topBar.animateRightButtons(options.animateRightButtons.isTrue()); + topBarController.animateRightButtons(options.animateRightButtons.isTrue()); if (options.animateLeftButtons.hasValue()) - topBar.animateLeftButtons(options.animateLeftButtons.isTrue()); - topBar.setOverflowButtonColor(options.rightButtonColor.get(Color.BLACK)); - } + topBarController.animateLeftButtons(options.animateLeftButtons.isTrue()); - private List getOrCreateButtonControllersByInstanceId(@Nullable Map currentButtons, @Nullable List buttons) { - if (buttons == null) return null; - Map result = new LinkedHashMap<>(); - forEach(buttons, b -> result.put(b.instanceId, getOrDefault(currentButtons, b.instanceId, () -> createButtonController(b)))); - return new ArrayList<>(result.values()); - } + applyRightButtonsOptions(options, child); - private List getOrCreateButtonControllers(@Nullable Map currentButtons, @NonNull List buttons) { - ArrayList result = new ArrayList<>(); - for (ButtonOptions b : buttons) { - result.add(take(first(perform(currentButtons, null, Map::values), button -> button.getButton().equals(b)), createButtonController(b))); + applyLeftButtonsOptions(options, child); + + if (options.buttons.back.visible.isTrue() && !options.buttons.hasLeftButtons()) { + topBarController.setBackButton(createButtonController(options.buttons.back)); } - return result; + + topBar.setOverflowButtonColor(options.rightButtonColor.get(Color.BLACK)); } private ButtonController createButtonController(ButtonOptions button) { @@ -419,8 +366,13 @@ private void mergeOrientation(OrientationOptions orientationOptions) { } private void mergeButtons(TopBarOptions options, TopBarOptions optionsToMerge, View child, StackController stack) { - mergeRightButtons(options, optionsToMerge.buttons, child); - mergeLeftButton(options, optionsToMerge.buttons, child); + if (optionsToMerge.animateRightButtons.hasValue()) + topBarController.animateRightButtons(optionsToMerge.animateRightButtons.isTrue()); + if (optionsToMerge.animateLeftButtons.hasValue()) + topBarController.animateLeftButtons(optionsToMerge.animateLeftButtons.isTrue()); + + mergeRightButtonsOptions(options, optionsToMerge.buttons, child); + mergeLeftButtonsOptions(options, optionsToMerge.buttons, child); mergeLeftButtonsColor(child, optionsToMerge.leftButtonColor, optionsToMerge.leftButtonDisabledColor); mergeRightButtonsColor(child, optionsToMerge.rightButtonColor, optionsToMerge.rightButtonDisabledColor); mergeBackButton(optionsToMerge.buttons, stack); @@ -428,7 +380,7 @@ private void mergeButtons(TopBarOptions options, TopBarOptions optionsToMerge, V private void mergeLeftButtonsColor(View child, ThemeColour color, ThemeColour disabledColor) { if (color.hasValue() || disabledColor.hasValue()) { - Map stringButtonControllerMap = componentLeftButtons.get(child); + Map stringButtonControllerMap = leftButtonControllers.get(child); if (stringButtonControllerMap != null) { forEach(stringButtonControllerMap.values(), (btnController) -> { if (color.hasValue()) { @@ -444,7 +396,7 @@ private void mergeLeftButtonsColor(View child, ThemeColour color, ThemeColour di private void mergeRightButtonsColor(View child, ThemeColour color, ThemeColour disabledColor) { if (color.hasValue() || disabledColor.hasValue()) { - Map stringButtonControllerMap = componentRightButtons.get(child); + Map stringButtonControllerMap = rightButtonControllers.get(child); if (stringButtonControllerMap != null) { forEach(stringButtonControllerMap.values(), (btnController) -> { if (color.hasValue()) { @@ -458,40 +410,65 @@ private void mergeRightButtonsColor(View child, ThemeColour color, ThemeColour d } } - private void mergeRightButtons(TopBarOptions options, TopBarButtons buttons, View child) { - if (buttons.right == null) return; - List rightButtons = mergeButtonsWithColor(buttons.right, options.rightButtonColor, options.rightButtonDisabledColor); - List toMerge = getOrCreateButtonControllers(componentRightButtons.get(child), rightButtons); - List toRemove = difference(currentRightButtons, toMerge, ButtonController::areButtonsEqual); - forEach(toRemove, ButtonController::destroy); + private void applyLeftButtonsOptions(TopBarOptions options, ViewController child) { + if (options.buttons.left != null) { + List leftButtons = mergeButtonsWithColor(options.buttons.left, + options.leftButtonColor + , options.leftButtonDisabledColor); + final ViewGroup childView = child.getView(); + final Map btnControllers = getOrCreateButtonControllerMap(childView, leftButtonControllers); + topBarController.applyLeftButtonsOptions(btnControllers, leftButtons, this::createButtonController); + } else { + topBarController.clearLeftButtons(); + } + } - if (!CollectionUtils.equals(currentRightButtons, toMerge)) { - componentRightButtons.put(child, keyBy(toMerge, ButtonController::getButtonInstanceId)); - topBarController.mergeRightButtons(toMerge, toRemove); - currentRightButtons = toMerge; + private void applyRightButtonsOptions(TopBarOptions options, ViewController child) { + if (options.buttons.right != null) { + List rightButtons = mergeButtonsWithColor(options.buttons.right, + options.rightButtonColor + , options.rightButtonDisabledColor); + final ViewGroup childView = child.getView(); + final Map btnControllers = getOrCreateButtonControllerMap(childView, rightButtonControllers); + topBarController.applyRightButtonsOptions(btnControllers, rightButtons, this::createButtonController); + } else { + topBarController.clearRightButtons(); } + } + + private void mergeRightButtonsOptions(TopBarOptions options, TopBarButtons buttons, View child) { + if (buttons.right == null) return; + List rightButtons = mergeButtonsWithColor(buttons.right, options.rightButtonColor, + options.rightButtonDisabledColor); + final Map btnControllers = getOrCreateButtonControllerMap(child, rightButtonControllers); + topBarController.mergeRightButtonsOptions(btnControllers, rightButtons, this::createButtonController); if (options.rightButtonColor.hasValue()) topBar.setOverflowButtonColor(options.rightButtonColor.get()); } - private void mergeLeftButton(TopBarOptions options, TopBarButtons buttons, View child) { + private void mergeLeftButtonsOptions(TopBarOptions options, TopBarButtons buttons, View child) { if (buttons.left == null) return; - List leftButtons = mergeButtonsWithColor(buttons.left, options.leftButtonColor, options.leftButtonDisabledColor); - List toMerge = getOrCreateButtonControllers(componentLeftButtons.get(child), leftButtons); - List toRemove = difference(currentLeftButtons, toMerge, ButtonController::areButtonsEqual); - forEach(toRemove, ButtonController::destroy); - if (!CollectionUtils.equals(currentLeftButtons, toMerge)) { - componentLeftButtons.put(child, keyBy(toMerge, ButtonController::getButtonInstanceId)); - topBarController.mergeLeftButtons(toMerge, toRemove); - currentLeftButtons = toMerge; - } + List leftButtons = mergeButtonsWithColor(buttons.left, options.leftButtonColor, + options.leftButtonDisabledColor); + final Map btnControllers = getOrCreateButtonControllerMap(child, leftButtonControllers); + topBarController.mergeLeftButtonsOptions(btnControllers, leftButtons, this::createButtonController); + if (options.leftButtonColor.hasValue()) topBar.setOverflowButtonColor(options.leftButtonColor.get()); + } + + @NonNull + private Map getOrCreateButtonControllerMap(View child, Map> buttonControllers) { + final Map controllerMap = buttonControllers.get(child); + final Map btnControllers = controllerMap != null ? controllerMap : new HashMap<>(); + if (controllerMap == null) + buttonControllers.put(child, btnControllers); + return btnControllers; } private void mergeBackButton(TopBarButtons buttons, StackController stack) { if (buttons.back.hasValue() && isNullOrEmpty(buttons.left)) { if (buttons.back.visible.isFalse()) { - topBar.clearBackButton(); + topBarController.clearBackButton(); } else if (stack.size() > 1) { - topBar.setBackButton(createButtonController(buttons.back)); + topBarController.setBackButton(createButtonController(buttons.back)); } } } @@ -517,13 +494,10 @@ private void mergeTopBarOptions(TopBarOptions resolveOptions, Options options, S if (topBarOptions.topMargin.hasValue() && topBar.getLayoutParams() instanceof MarginLayoutParams) { ((MarginLayoutParams) topBar.getLayoutParams()).topMargin = UiUtils.dpToPx(activity, topBarOptions.topMargin.get()); } - mergeStatusBarDrawBehindOptions(resolveOptions,options); + mergeStatusBarDrawBehindOptions(resolveOptions, options); if (topBarOptions.title.height.hasValue()) topBar.setTitleHeight(topBarOptions.title.height.get()); if (topBarOptions.title.topMargin.hasValue()) topBar.setTitleTopMargin(topBarOptions.title.topMargin.get()); - if (topBarOptions.animateLeftButtons.hasValue()) - topBar.animateLeftButtons(topBarOptions.animateLeftButtons.isTrue()); - if (topBarOptions.animateRightButtons.hasValue()) - topBar.animateRightButtons(topBarOptions.animateRightButtons.isTrue()); + if (topBarOptions.title.component.hasValue()) { TitleBarReactViewController controller = findTitleComponent(topBarOptions.title.component); if (controller == null) { @@ -646,8 +620,8 @@ public List getComponentButtons(View child, List componentLeftButtons.get(child).put(key, leftController)); - forEach(componentRightButtons.get(child).keySet(), (key) -> componentRightButtons.get(child).put(key, rightController)); + forEach(leftButtonControllers.get(child).keySet(), (key) -> leftButtonControllers.get(child).put(key, leftController)); + forEach(rightButtonControllers.get(child).keySet(), (key) -> rightButtonControllers.get(child).put(key, rightController)); } @@ -657,11 +631,11 @@ public void applyTopInsets(StackController stack, ViewController child) { } private List getRightButtons(View child) { - return componentRightButtons.containsKey(child) ? new ArrayList<>(componentRightButtons.get(child).values()) : null; + return rightButtonControllers.containsKey(child) ? new ArrayList<>(rightButtonControllers.get(child).values()) : null; } private List getLeftButtons(View child) { - return componentLeftButtons.containsKey(child) ? new ArrayList<>(componentLeftButtons.get(child).values()) : null; + return leftButtonControllers.containsKey(child) ? new ArrayList<>(leftButtonControllers.get(child).values()) : null; } private void applyStatusBarInsets(StackController stack, ViewController child) { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/TopBarController.kt b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/TopBarController.kt index f0ac09d20b7..09a27f6f585 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/TopBarController.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/TopBarController.kt @@ -2,13 +2,16 @@ package com.reactnativenavigation.viewcontrollers.stack.topbar import android.animation.Animator import android.content.Context +import android.graphics.Color import android.view.MenuItem import android.view.View +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager import androidx.viewpager.widget.ViewPager import com.reactnativenavigation.options.Alignment import com.reactnativenavigation.options.AnimationOptions +import com.reactnativenavigation.options.ButtonOptions import com.reactnativenavigation.options.Options -import com.reactnativenavigation.utils.CollectionUtils.forEachIndexed import com.reactnativenavigation.utils.ViewUtils import com.reactnativenavigation.utils.resetViewProperties import com.reactnativenavigation.viewcontrollers.stack.topbar.button.ButtonController @@ -17,12 +20,13 @@ import com.reactnativenavigation.views.stack.StackLayout import com.reactnativenavigation.views.stack.topbar.TopBar import com.reactnativenavigation.views.stack.topbar.titlebar.ButtonBar +const val DEFAULT_BORDER_COLOR = Color.BLACK open class TopBarController(private val animator: TopBarAnimator = TopBarAnimator()) { lateinit var view: TopBar private lateinit var leftButtonBar: ButtonBar private lateinit var rightButtonBar: ButtonBar - + private val buttonsTransition = AutoTransition() val height: Int get() = view.height @@ -54,26 +58,26 @@ open class TopBarController(private val animator: TopBarAnimator = TopBarAnimato fun getPushAnimation(appearingOptions: Options, additionalDy: Float = 0f): Animator? { if (appearingOptions.topBar.animate.isFalse) return null return animator.getPushAnimation( - appearingOptions.animations.push.topBar, - appearingOptions.topBar.visible, - additionalDy + appearingOptions.animations.push.topBar, + appearingOptions.topBar.visible, + additionalDy ) } fun getPopAnimation(appearingOptions: Options, disappearingOptions: Options): Animator? { if (appearingOptions.topBar.animate.isFalse) return null return animator.getPopAnimation( - disappearingOptions.animations.pop.topBar, - appearingOptions.topBar.visible + disappearingOptions.animations.pop.topBar, + appearingOptions.topBar.visible ) } fun getSetStackRootAnimation(appearingOptions: Options, additionalDy: Float = 0f): Animator? { if (appearingOptions.topBar.animate.isFalse) return null return animator.getSetStackRootAnimation( - appearingOptions.animations.setStackRoot.topBar, - appearingOptions.topBar.visible, - additionalDy + appearingOptions.animations.setStackRoot.topBar, + appearingOptions.topBar.visible, + additionalDy ) } @@ -105,25 +109,182 @@ open class TopBarController(private val animator: TopBarAnimator = TopBarAnimato view.alignTitleComponent(alignment) } - fun applyRightButtons(toAdd: List) { + fun clearRightButtons() { view.clearRightButtons() - toAdd.reversed().forEachIndexed { i, b -> b.addToMenu(rightButtonBar, i * 10) } } - fun mergeRightButtons(toAdd: List, toRemove: List) { - toRemove.forEach { view.removeRightButton(it) } - toAdd.reversed().forEachIndexed { i, b -> b.addToMenu(rightButtonBar, i * 10) } + fun clearLeftButtons() { + view.clearLeftButtons() } - open fun applyLeftButtons(toAdd: List) { + fun clearBackButton() { view.clearBackButton() - view.clearLeftButtons() - forEachIndexed(toAdd) { b: ButtonController, i: Int -> b.addToMenu(leftButtonBar, i * 10) } } - open fun mergeLeftButtons(toAdd: List, toRemove: List) { - view.clearBackButton() - toRemove.forEach { view.removeLeftButton(it) } - forEachIndexed(toAdd) { b: ButtonController, i: Int -> b.addToMenu(leftButtonBar, i * 10) } + fun setBackButton(backButton: ButtonController?) { + backButton?.let { view.setBackButton(it) } + } + + fun animateRightButtons(shouldAnimate: Boolean) { + view.animateRightButtons(shouldAnimate) + } + + fun animateLeftButtons(shouldAnimate: Boolean) { + view.animateLeftButtons(shouldAnimate) + } + + fun mergeRightButtonsOptions( + btnControllers: MutableMap, + rightButtons: List, + controllerCreator: (ButtonOptions) -> ButtonController + ) { + mergeButtonOptions(btnControllers, rightButtons.reversed(), controllerCreator, rightButtonBar) + } + + fun mergeLeftButtonsOptions( + btnControllers: MutableMap, + leftButtons: List, + controllerCreator: (ButtonOptions) -> ButtonController + ) { + clearBackButton() + mergeButtonOptions(btnControllers, leftButtons, controllerCreator, leftButtonBar) + } + + fun applyRightButtonsOptions( + btnControllers: MutableMap, + rightButtons: List, + controllerCreator: (ButtonOptions) -> ButtonController + ) { + applyButtonsOptions( + btnControllers, + rightButtons.reversed(), + controllerCreator, + rightButtonBar + ) + } + + fun applyLeftButtonsOptions( + btnControllers: MutableMap, + leftButtons: List, + controllerCreator: (ButtonOptions) -> ButtonController + ) { + applyButtonsOptions(btnControllers, leftButtons, controllerCreator, leftButtonBar) + } + + private fun applyButtonsOptions( + btnControllers: MutableMap, + buttons: List, + controllerCreator: (ButtonOptions) -> ButtonController, + buttonBar: ButtonBar + ) { + if (buttonBar.shouldAnimate) + TransitionManager.beginDelayedTransition(buttonBar, buttonsTransition) + + buttonBar.clearButtons() + buttons.forEachIndexed { index, it -> + val order = index * 10 + val newController = if (btnControllers.containsKey(it.id)) { + btnControllers.remove(it.id) + } else { + controllerCreator(it) + }!! + + newController.addToMenu(buttonBar, order) + btnControllers[it.id] = newController + } + } + + + private fun mergeButtonOptions( + btnControllers: MutableMap, + buttons: List, + controllerCreator: (ButtonOptions) -> ButtonController, + buttonBar: ButtonBar + ) { + fun hasChangedOrder(): Boolean { + val values = btnControllers.values + return buttons.filterIndexed { index, buttonOptions -> + val buttonController = btnControllers[buttonOptions.id] + values.indexOf(buttonController) == index + }.size != buttons.size + } + + fun sameIdDifferentCompId( + toUpdate: MutableMap, + ctrl: Map.Entry, + buttons: List + ) = if (toUpdate.containsKey(ctrl.key) + && ctrl.value.button.hasComponent() + && buttons[toUpdate[ctrl.key]!!].component.componentId != ctrl.value.button.component.componentId + ) { + toUpdate.remove(ctrl.key) + true + } else false + + val requestedButtons = buttons.mapIndexed { index, buttonOptions -> buttonOptions.id to index }.toMap() + var toUpdate = requestedButtons.filter { + btnControllers[it.key]?.areButtonOptionsChanged(buttons[it.value]) ?: false + }.toMutableMap() + var toAdd = requestedButtons.filter { !btnControllers.containsKey(it.key) } + var toRemove = btnControllers.filter { ctrl -> !requestedButtons.containsKey(ctrl.key) } + val toDestroy = btnControllers.filter { ctrl -> sameIdDifferentCompId(toUpdate, ctrl, buttons) } + .toMutableMap().apply { this.putAll(toRemove) } + + fun needsRebuild(): Boolean { + return if (toUpdate.size == buttons.size) { + hasChangedOrder() + } else toAdd.isNotEmpty() || toRemove.isNotEmpty() + } + + if (needsRebuild()) { + toUpdate = mutableMapOf() + toAdd = requestedButtons + toRemove = btnControllers.toMap() + if (buttonBar.shouldAnimate) + TransitionManager.beginDelayedTransition(buttonBar, buttonsTransition) + } + + toUpdate.forEach { + val button = buttons[it.value] + btnControllers[button.id]?.mergeButtonOptions(button, buttonBar) + } + toRemove.forEach { + buttonBar.removeButton(it.value.buttonIntId) + } + toDestroy.values.forEach { + btnControllers.remove(it.id) + it.destroy() + } + toAdd.forEach { + val button = buttons[it.value] + val order = it.value * 10 + val newController = btnControllers[button.id] ?: controllerCreator(button) + newController.addToMenu(buttonBar, order) + btnControllers[button.id] = newController + } + } + + + fun onConfigurationChanged( + options: Options, + leftBtnControllers: MutableMap?, + rightBtnControllers: MutableMap? + ) { + leftBtnControllers?.values?.forEach { + it.onConfigurationChanged(leftButtonBar) + } + rightBtnControllers?.values?.forEach { + it.onConfigurationChanged(rightButtonBar) + } + + view.setOverflowButtonColor(options.topBar.rightButtonColor.get(Color.BLACK)!!) + view.applyTopTabsColors( + options.topTabs.selectedTabColor, + options.topTabs.unselectedTabColor + ) + view.setBorderColor(options.topBar.borderColor.get(DEFAULT_BORDER_COLOR)!!) + view.setBackgroundColor(options.topBar.background.color.get(Color.WHITE)!!) + view.setTitleTextColor(options.topBar.title.color.get(TopBar.DEFAULT_TITLE_COLOR)!!) + view.setSubtitleColor(options.topBar.subtitle.color.get(TopBar.DEFAULT_TITLE_COLOR)!!) } } \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonController.kt b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonController.kt index b18cb11b987..22ce11f031e 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonController.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonController.kt @@ -71,6 +71,10 @@ open class ButtonController(activity: Activity, return if (other.id != id) false else button.equals(other.button) } + fun areButtonOptionsChanged(otherOptions:ButtonOptions):Boolean{ + return otherOptions.id == id && !button.equals(otherOptions) + } + fun applyNavigationIcon(toolbar: Toolbar) { presenter.applyNavigationIcon(toolbar) { onPressListener.onPress(it) @@ -83,13 +87,30 @@ open class ButtonController(activity: Activity, fun addToMenu(buttonBar: ButtonBar, order: Int) { if (button.component.hasValue() && buttonBar.containsButton(menuItem, order)) return - buttonBar.menu.removeItem(button.intId) - menuItem = buttonBar.addButton(Menu.NONE, + buttonBar.menu.removeItem(button.intId) + menuItem = buttonBar.addButton(Menu.NONE, button.intId, order, presenter.styledText)?.also { menuItem -> - menuItem.setOnMenuItemClickListener(this@ButtonController) - presenter.applyOptions(buttonBar, menuItem, this@ButtonController::getView) + menuItem.setOnMenuItemClickListener(this@ButtonController) + presenter.applyOptions(buttonBar, menuItem, this@ButtonController::getView) + } + } + + fun mergeButtonOptions(optionsToMerge: ButtonOptions,buttonBar: ButtonBar) { + button.mergeWith(optionsToMerge) + presenter.button = this.button + buttonBar.getButtonById(button.intId)?.let { + menuItem-> + presenter.applyOptions(buttonBar,menuItem,this::getView) } } + + fun onConfigurationChanged(buttonBar: ButtonBar) { + buttonBar.getButtonById(button.intId)?.let { + menuItem-> + presenter.applyOptions(buttonBar,menuItem,this::getView) + } + } + } \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonPresenter.kt b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonPresenter.kt index e1520a4cceb..5be22e2047e 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonPresenter.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonPresenter.kt @@ -25,7 +25,8 @@ import com.reactnativenavigation.utils.ViewUtils import com.reactnativenavigation.views.stack.topbar.titlebar.IconBackgroundDrawable import kotlin.math.max -open class ButtonPresenter(private val context: Context, private val button: ButtonOptions, private val iconResolver: IconResolver) { +open class ButtonPresenter(private val context: Context,var button: ButtonOptions, private val iconResolver: +IconResolver) { companion object { const val DISABLED_COLOR = Color.LTGRAY } @@ -47,6 +48,8 @@ open class ButtonPresenter(private val context: Context, private val button: But applyComponent(menuItem, viewCreator) applyAccessibilityLabel(menuItem) applyIcon(menuItem) + applyText(menuItem) + applyOptionsDirectlyOnView(toolbar, menuItem) { applyTestId(it) @@ -55,6 +58,11 @@ open class ButtonPresenter(private val context: Context, private val button: But } } + private fun applyText(menuItem: MenuItem) { + if (button.text.hasValue()) + menuItem.title = button.text.get() + } + fun applyColor(toolbar: Toolbar, menuItem: MenuItem, color: ThemeColour) { button.color = color applyIcon(menuItem) diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonSpan.kt b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonSpan.kt index bf10b685b0b..d8728494600 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonSpan.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/topbar/button/ButtonSpan.kt @@ -25,7 +25,7 @@ class ButtonSpan( fun apply(paint: Paint) { with(button.font) { - val typeface = getTypeface(typefaceLoader, Typeface.DEFAULT) + val typeface = getTypeface(typefaceLoader, paint.typeface) val fakeStyle = (paint.typeface?.style ?: 0) and (typeface?.style?.inv() ?: 1) if (fakeStyle and Typeface.BOLD != 0) paint.isFakeBoldText = true if (fakeStyle and Typeface.ITALIC != 0) paint.textSkewX = -0.25f diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/ButtonBar.kt b/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/ButtonBar.kt index c17afc67757..32c8cd23486 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/ButtonBar.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/ButtonBar.kt @@ -4,17 +4,15 @@ import android.content.Context import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.text.SpannableString -import android.transition.AutoTransition -import android.transition.TransitionManager import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.ActionMenuView import androidx.appcompat.widget.Toolbar +import androidx.core.view.children import com.reactnativenavigation.utils.ObjectUtils import com.reactnativenavigation.utils.ViewUtils import com.reactnativenavigation.viewcontrollers.stack.topbar.button.ButtonController - open class ButtonBar internal constructor(context: Context) : Toolbar(context) { var shouldAnimate: Boolean=false @@ -43,23 +41,17 @@ open class ButtonBar internal constructor(context: Context) : Toolbar(context) { get() = menu.size() fun addButton(menuItem: Int, intId: Int, order: Int, styledText: SpannableString): MenuItem? { - if(shouldAnimate) - TransitionManager.beginDelayedTransition(this,AutoTransition()) return this.menu?.add(menuItem, - intId, - order, - styledText) + intId, + order, + styledText) } fun removeButton(buttonId: Int) { - if(shouldAnimate) - TransitionManager.beginDelayedTransition(this,AutoTransition()) menu.removeItem(buttonId) } open fun clearButtons() { - if(shouldAnimate) - TransitionManager.beginDelayedTransition(this,AutoTransition()) clearBackButton() if (menu.size() > 0) menu.clear() } @@ -68,6 +60,9 @@ open class ButtonBar internal constructor(context: Context) : Toolbar(context) { return menu.getItem(index) } + fun getButtonById(id: Int): MenuItem? { + return menu.children.firstOrNull { it.itemId == id } + } fun containsButton(menuItem: MenuItem?, order: Int): Boolean { return menuItem != null && menu.findItem(menuItem.itemId) != null && menuItem.order == order } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleAndButtonsContainer.kt b/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleAndButtonsContainer.kt index 196e580b5af..52d687e853e 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleAndButtonsContainer.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleAndButtonsContainer.kt @@ -76,8 +76,6 @@ class TitleAndButtonsContainer(context: Context) : ViewGroup(context) { fun setSubTitleTextAlignment(alignment: Alignment) = titleSubTitleBar.setSubTitleAlignment(alignment) - fun setTitleTextAlignment(alignment: Alignment) = titleSubTitleBar.setTitleAlignment(alignment) - fun setBackgroundColor(color: ThemeColour) = if (color.hasValue()) setBackgroundColor(color.get()) else Unit fun setTitleFontSize(size: Float) = titleSubTitleBar.setTitleFontSize(size) diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleSubTitleLayout.kt b/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleSubTitleLayout.kt index 62c46250f64..3af4d77b903 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleSubTitleLayout.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleSubTitleLayout.kt @@ -38,13 +38,6 @@ class TitleSubTitleLayout(context: Context) : LinearLayout(context) { } } - fun setTitleAlignment(alignment: Alignment) { - if (alignment == Alignment.Center) { - (this.titleTextView.layoutParams as LayoutParams).gravity = Gravity.CENTER - } else { - (this.titleTextView.layoutParams as LayoutParams).gravity = Gravity.START or Gravity.CENTER_VERTICAL - } - } fun setTitleFontSize(size: Float) = titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, size) diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/utils/LayoutFactoryTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/utils/LayoutFactoryTest.java index 79fa1378c0a..efa43b037e3 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/utils/LayoutFactoryTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/utils/LayoutFactoryTest.java @@ -15,14 +15,19 @@ import java.util.HashMap; import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.assertj.core.api.Java6Assertions.fail; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class LayoutFactoryTest extends BaseTest { private LayoutFactory uut; + private ReactInstanceManager mockReactInstanceManager; @Override public void beforeEach() { - uut = new LayoutFactory(mock(ReactInstanceManager.class)); + super.beforeEach(); + mockReactInstanceManager = mock(ReactInstanceManager.class); + uut = new LayoutFactory(mockReactInstanceManager); uut.init( newActivity(), Mockito.mock(EventEmitter.class), @@ -36,6 +41,16 @@ public void sanity() throws JSONException { assertThat(uut.create(component())).isNotNull(); } + @Test + public void shouldParseOptionsWhenReactContextIsNull() { + when(mockReactInstanceManager.getCurrentReactContext()).thenReturn(null); + try { + uut.create(component()); + } catch (Exception e) { + fail("Create should not fail! when react instance has null context"); + } + } + @Test public void defaultOptionsAreNotNull() { assertThat(uut.getDefaultOptions()).isNotNull(); @@ -50,6 +65,14 @@ public void defaultOptionsAreNotNull() { } private LayoutNode component() throws JSONException { - return new LayoutNode("Component1", LayoutNode.Type.Component, new JSONObject().put("name", "com.component"), null); + final JSONObject component = new JSONObject(); + final JSONObject layout = new JSONObject(); + final JSONObject backgroundColor = new JSONObject(); + backgroundColor.put("dark",0); + backgroundColor.put("light",1); + layout.put("backgroundColor",backgroundColor ); + component.put("name", "com.component"); + component.put("options",new JSONObject().put("layout", layout)); + return new LayoutNode("Component1", LayoutNode.Type.Component, component, null); } } diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleAndButtonsMeasurer.kt b/lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleAndButtonsMeasurerTest.kt similarity index 99% rename from lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleAndButtonsMeasurer.kt rename to lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleAndButtonsMeasurerTest.kt index d8509ee5089..3908a776979 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleAndButtonsMeasurer.kt +++ b/lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleAndButtonsMeasurerTest.kt @@ -2,13 +2,13 @@ package com.reactnativenavigation.utils import com.reactnativenavigation.BaseTest import com.reactnativenavigation.views.stack.topbar.titlebar.DEFAULT_LEFT_MARGIN_PX +import com.reactnativenavigation.views.stack.topbar.titlebar.resolveHorizontalTitleBoundsLimit import com.reactnativenavigation.views.stack.topbar.titlebar.resolveLeftButtonsBounds import com.reactnativenavigation.views.stack.topbar.titlebar.resolveRightButtonsBounds -import com.reactnativenavigation.views.stack.topbar.titlebar.resolveHorizontalTitleBoundsLimit import org.junit.Test import kotlin.test.assertEquals -class TitleAndButtonsMeasurer : BaseTest() { +class TitleAndButtonsMeasurerTest : BaseTest() { private val parentWidth = 1080 @Test diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java index 6450567de2d..fedd2e2f773 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java @@ -50,6 +50,7 @@ public class ModalPresenterTest extends BaseTest { @Override public void beforeEach() { + super.beforeEach(); Activity activity = newActivity(); ChildControllersRegistry childRegistry = new ChildControllersRegistry(); @@ -210,6 +211,7 @@ public void dismissModal_previousViewIsAddedAtIndex0() { uut.setRootLayout(spy); uut.showModal(modal1, root, new CommandListenerAdapter()); + idleMainLooper(); uut.dismissModal(modal1, root, root, new CommandListenerAdapter()); verify(spy).addView(root.getView(), 0); @@ -237,6 +239,7 @@ public void dismissModal_previousModalIsAddedBackToHierarchy() { verify(modal1).onViewWillAppear(); uut.showModal(modal2, modal1, new CommandListenerAdapter()); + idleMainLooper(); assertThat(modal1.getView().getParent()).isNull(); Shadows.shadowOf(Looper.getMainLooper()).idle(); @@ -253,6 +256,7 @@ public void dismissModal_previousControllerIsNotAddedIfDismissedModalIsNotTop() uut.showModal(modal1, root, new CommandListenerAdapter()); uut.showModal(modal2, modal1, new CommandListenerAdapter()); + idleMainLooper(); assertThat(modal1.getView().getParent()).isNull(); assertThat(root.getView().getParent()).isNull(); diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java index 733f517d69d..81affe984a6 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java @@ -13,7 +13,7 @@ import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry; import com.reactnativenavigation.viewcontrollers.stack.StackController; import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController; - +import com.reactnativenavigation.options.TransitionAnimationOptions; import org.junit.Test; import org.mockito.Mockito; @@ -54,6 +54,7 @@ public class ModalStackTest extends BaseTest { @Override public void beforeEach() { + super.beforeEach(); activity = newActivity(); childRegistry = new ChildControllersRegistry(); root = new SimpleViewController(activity, childRegistry, "root", new Options()); @@ -81,6 +82,16 @@ public void beforeEach() { .build(); } + @Test + public void showModal_DidAppearEventShouldWaitForReactViewToBeShown(){ + CommandListener listener = spy(new CommandListenerAdapter()); + uut.showModal(modal1, root, listener); + verify(modal1).addOnAppearedListener(any()); + verify(listener).onSuccess(modal1.getId()); + idleMainLooper(); + verify(modal1).onViewDidAppear(); + } + @Test public void modalRefIsSaved() { disableShowModalAnimation(modal1); @@ -94,6 +105,7 @@ public void modalRefIsSaved() { public void showModal() { CommandListener listener = spy(new CommandListenerAdapter()); uut.showModal(modal1, root, listener); + idleMainLooper(); verify(listener).onSuccess(modal1.getId()); verify(modal1).onViewDidAppear(); assertThat(uut.size()).isOne(); diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java index 2490271cb42..9f43fe03868 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java @@ -507,7 +507,7 @@ public void popTo_FromCorrectStackUpToChild() { StackController stack2 = newStack(child2, child3, child4); BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2)); uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager); - + idleMainLooper(); CommandListenerAdapter listener = spy(new CommandListenerAdapter() { @Override public void onSuccess(String childId) { @@ -670,7 +670,7 @@ public void pop_FromCorrectStackByFindingChildId_Promise() { final StackController stack2 = newStack(child2, child3); BottomTabsController bottomTabsController = newTabs(Arrays.asList(stack1, stack2)); uut.setRoot(bottomTabsController, new CommandListenerAdapter(), reactInstanceManager); - + idleMainLooper(); CommandListenerAdapter listener = spy(new CommandListenerAdapter() { @Override public void onSuccess(String childId) { diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.kt b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.kt index 19bc3c6e0e6..cd36ab5f353 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.kt +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.kt @@ -197,7 +197,7 @@ class StackControllerTest : BaseTest() { fun setRoot_pushDuringSetRootAnimationShouldNotCrash() { uut.push(child1, CommandListenerAdapter()) uut.push(child2, CommandListenerAdapter()) - + idleMainLooper() uut.setRoot(listOf(child1), CommandListenerAdapter()) uut.push(child3, CommandListenerAdapter()) assertThat(uut.currentChild).isEqualTo(child3) @@ -281,6 +281,7 @@ class StackControllerTest : BaseTest() { disablePushAnimation(child1, child2) uut.push(child1, CommandListenerAdapter()) // Initialize stack with a child uut.push(child2, CommandListenerAdapter()) + idleMainLooper() verify(child2).onViewDidAppear() } @@ -608,8 +609,9 @@ class StackControllerTest : BaseTest() { assertNotChildOf(uut.view, child1.view) uut.push(child1, CommandListenerAdapter()) assertIsChild(uut.view, child1.view) - + idleMainLooper() uut.push(child2, CommandListenerAdapter()) + idleMainLooper() assertIsChild(uut.view, child2) assertNotChildOf(uut.view, child1) } @@ -663,7 +665,7 @@ class StackControllerTest : BaseTest() { val child1View: View = child1.view uut.push(child1, CommandListenerAdapter()) uut.push(child2, CommandListenerAdapter()) - + idleMainLooper() assertIsChild(uut.view, child2View) assertNotChildOf(uut.view, child1View) uut.pop(Options.EMPTY, CommandListenerAdapter()) @@ -715,7 +717,7 @@ class StackControllerTest : BaseTest() { uut.push(child2, CommandListenerAdapter()) uut.push(child3, CommandListenerAdapter()) uut.push(child4, CommandListenerAdapter()) - + idleMainLooper() uut.popTo(child2, Options.EMPTY, CommandListenerAdapter()) verify(animator, never()).pop(any(), eq(child1), any(), any(), any()) verify(animator, never()).pop(any(), eq(child2), any(), any(), any()) @@ -729,6 +731,7 @@ class StackControllerTest : BaseTest() { uut.push(child1, mock()) uut.push(child2, mock()) uut.push(child3, mock()) + idleMainLooper() uut.popTo(child1, Options.EMPTY, mock()) animator.endPushAnimation(child3) assertContainsOnlyId(child1.id) @@ -759,8 +762,8 @@ class StackControllerTest : BaseTest() { disablePushAnimation(child1, child2, child3) uut.push(child1, CommandListenerAdapter()) uut.push(child2, CommandListenerAdapter()) - uut.push(child3, CommandListenerAdapter()) + idleMainLooper() uut.popToRoot(Options.EMPTY, object : CommandListenerAdapter() { override fun onSuccess(childId: String) { verify(animator).pop(eq(child1), eq(child3), any(), any(), any()) @@ -776,6 +779,7 @@ class StackControllerTest : BaseTest() { uut.push(child1, CommandListenerAdapter()) uut.push(child2, CommandListenerAdapter()) uut.push(child3, CommandListenerAdapter()) + idleMainLooper() uut.popToRoot(Options.EMPTY, object : CommandListenerAdapter() { override fun onSuccess(childId: String) { verify(child1, never()).destroy() @@ -811,6 +815,7 @@ class StackControllerTest : BaseTest() { uut.push(child1, mock()) uut.push(child2, mock()) uut.push(child3, mock()) + idleMainLooper() uut.popToRoot(Options.EMPTY, mock()) animator.endPushAnimation(child3) assertContainsOnlyId(child1.id) diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenterTest.kt b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenterTest.kt index 779d9b3b621..333b3cd2777 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenterTest.kt +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenterTest.kt @@ -17,7 +17,10 @@ import com.reactnativenavigation.options.params.* import com.reactnativenavigation.options.params.Number import com.reactnativenavigation.options.parsers.TypefaceLoader import com.reactnativenavigation.react.CommandListenerAdapter -import com.reactnativenavigation.utils.* +import com.reactnativenavigation.utils.CollectionUtils +import com.reactnativenavigation.utils.RenderChecker +import com.reactnativenavigation.utils.TitleBarHelper +import com.reactnativenavigation.utils.UiUtils import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry import com.reactnativenavigation.viewcontrollers.stack.topbar.TopBarController import com.reactnativenavigation.viewcontrollers.stack.topbar.button.ButtonController @@ -99,6 +102,7 @@ class StackPresenterTest : BaseTest() { @Test fun onConfigurationChange_shouldApplyColors() { + parent.setRoot(listOf(child), CommandListenerAdapter()) val options = Options.EMPTY.copy() options.topBar.borderColor = ThemeColour.of(Color.BLACK, Color.RED) options.topBar.background = TopBarBackgroundOptions().apply { @@ -122,7 +126,7 @@ class StackPresenterTest : BaseTest() { options.topTabs.unselectedTabColor = ThemeColour.of(Color.BLACK, Color.RED) mockConfiguration.uiMode = Configuration.UI_MODE_NIGHT_NO - uut.onConfigurationChanged(options) + uut.onConfigurationChanged(options, getCurrentChild()) verify(topBar).setTitleTextColor(Color.BLACK) verify(topBar).setSubtitleColor(Color.BLACK) @@ -133,7 +137,7 @@ class StackPresenterTest : BaseTest() { verify(topBar).setBackButton(any()) mockConfiguration.uiMode = Configuration.UI_MODE_NIGHT_YES - uut.onConfigurationChanged(options) + uut.onConfigurationChanged(options, getCurrentChild()) verify(topBar).setTitleTextColor(Color.RED) verify(topBar).setSubtitleColor(Color.RED) @@ -147,15 +151,18 @@ class StackPresenterTest : BaseTest() { @Test fun onConfigurationChange_shouldApplyColorsOnTopBarButtons() { + parent.setRoot(listOf(child), CommandListenerAdapter()) + val options = Options.EMPTY.copy() options.topBar.buttons.left = arrayListOf(ButtonOptions()) options.topBar.buttons.right = arrayListOf(ButtonOptions()) - uut.applyChildOptions(options,parent,child) - uut.onConfigurationChanged(options) + uut.applyChildOptions(options,parent,child) + verify(topBarController, times(1)).applyRightButtonsOptions(any(),any(),any()) + verify(topBarController, times(1)).applyLeftButtonsOptions(any(),any(),any()) - verify(topBarController, times(2)).applyRightButtons(any()) - verify(topBarController, times(2)).applyLeftButtons(any()) + uut.onConfigurationChanged(options, getCurrentChild()) + verify(topBarController, times(1)).onConfigurationChanged(any(), any(), any()) } @Test @@ -252,19 +259,19 @@ class StackPresenterTest : BaseTest() { @Test fun mergeButtons() { uut.mergeChildOptions(EMPTY_OPTIONS, EMPTY_OPTIONS, parent, child) - verify(topBarController, never()).applyRightButtons(any()) - verify(topBarController, never()).applyLeftButtons(any()) + verify(topBarController, never()).mergeLeftButtonsOptions(any(),any(),any()) + verify(topBarController, never()).mergeRightButtonsOptions(any(),any(),any()) val options = Options() val button = ButtonOptions() button.text = Text("btn") options.topBar.buttons.right = ArrayList(setOf(button)) uut.mergeChildOptions(options, EMPTY_OPTIONS, parent, child) - verify(topBarController).mergeRightButtons(any(), any()) + verify(topBarController).mergeRightButtonsOptions(any(), any(),any()) options.topBar.buttons.left = ArrayList(setOf(button)) uut.mergeChildOptions(options, EMPTY_OPTIONS, parent, child) - verify(topBarController).mergeLeftButtons(any(), any()) + verify(topBarController).mergeLeftButtonsOptions(any(), any(), any()) } @Test @@ -313,8 +320,8 @@ class StackPresenterTest : BaseTest() { toApply.topBar.buttons.right = arrayListOf(textBtn1, componentBtn1) uut.applyChildOptions(toApply, parent, child) - val captor1 = argumentCaptor>() - verify(topBarController).applyRightButtons(captor1.capture()) + val captor1 = argumentCaptor>() + verify(topBarController).applyRightButtonsOptions(any(),captor1.capture(), any()) assertThat(topBar.rightButtonBar.menu.size()).isEqualTo(2) val appliedButtons = captor1.firstValue @@ -325,12 +332,12 @@ class StackPresenterTest : BaseTest() { uut.mergeChildOptions(toMerge, Options.EMPTY, parent, child) assertThat(topBar.rightButtonBar.menu.size()).isEqualTo(3) - val captor2 = argumentCaptor>() - verify(topBarController).mergeRightButtons(captor2.capture(), any()) + val captor2 = argumentCaptor>() + verify(topBarController).mergeRightButtonsOptions(any(),captor2.capture(), any()) val mergedButtons = captor2.firstValue assertThat(mergedButtons).hasSize(3) - assertThat(appliedButtons[0]).isNotEqualTo(mergedButtons[0]) - assertThat(appliedButtons[1]).isEqualTo(mergedButtons[2]) + assertThat(appliedButtons[0].id).isNotEqualTo(mergedButtons[1].id) + assertThat(appliedButtons[1].id).isEqualTo(mergedButtons[2].id) } @Test @@ -357,15 +364,14 @@ class StackPresenterTest : BaseTest() { options.topBar.buttons.left = ArrayList(listOf(textBtn2)) uut.applyChildOptions(options, parent, child) ShadowLooper.idleMainLooper() - verify(topBar, times(1)).clearLeftButtons() - verify(topBar, times(1)).clearBackButton() + verify(topBarController, times(1)).applyLeftButtonsOptions(any(), any(), any()) + verify(topBar, never()).setBackButton(any()) val backButtonHidden = Options() backButtonHidden.topBar.buttons.back.setHidden() uut.mergeChildOptions(backButtonHidden, options, parent, child) ShadowLooper.idleMainLooper() - verify(topBar, times(1)).clearLeftButtons() - verify(topBar, times(2)).clearBackButton() + verify(topBar, times(1)).clearBackButton() } @Test @@ -400,7 +406,7 @@ class StackPresenterTest : BaseTest() { assertThat(toMerge.topBar.buttons.back.hasValue()).isTrue() uut.mergeChildOptions(toMerge, Options.EMPTY, parent, child) - verify(topBarController).mergeLeftButtons(any(), any()) + verify(topBarController).mergeLeftButtonsOptions(any(), any(),any()) verify(topBar, never()).clearLeftButtons() } @@ -662,15 +668,15 @@ class StackPresenterTest : BaseTest() { options.topBar.buttons.left = ArrayList() options.topBar.buttons.left!!.add(leftButton) uut.applyChildOptions(options, parent, child) - val rightCaptor = argumentCaptor>() - verify(topBarController).applyRightButtons(rightCaptor.capture()) - assertThat(rightCaptor.firstValue[0].button.color.get()).isEqualTo(options.topBar.rightButtonColor.get()) - assertThat(rightCaptor.firstValue[1].button.color.get()).isEqualTo(options.topBar.rightButtonColor.get()) + val rightCaptor = argumentCaptor>() + verify(topBarController).applyRightButtonsOptions(any(),rightCaptor.capture(), any()) + assertThat(rightCaptor.firstValue[0].color.get()).isEqualTo(options.topBar.rightButtonColor.get()) + assertThat(rightCaptor.firstValue[1].color.get()).isEqualTo(options.topBar.rightButtonColor.get()) assertThat(rightCaptor.firstValue[0]).isNotEqualTo(rightButton1) assertThat(rightCaptor.firstValue[1]).isNotEqualTo(rightButton2) - val leftCaptor = argumentCaptor>() - verify(topBarController).applyLeftButtons(leftCaptor.capture()) - assertThat(leftCaptor.firstValue[0].button.color).isEqualTo(options.topBar.leftButtonColor) + val leftCaptor = argumentCaptor>() + verify(topBarController).applyLeftButtonsOptions(any(),leftCaptor.capture(),any()) + assertThat(leftCaptor.firstValue[0].color).isEqualTo(options.topBar.leftButtonColor) assertThat(leftCaptor.firstValue[0]).isNotEqualTo(leftButton) } @@ -790,15 +796,15 @@ class StackPresenterTest : BaseTest() { options2.topBar.buttons.left = ArrayList(listOf(leftButton)) uut.mergeChildOptions(options2, appliedOptions, parent, child) - val rightCaptor = argumentCaptor>() - verify(topBarController).mergeRightButtons(rightCaptor.capture(), any()) - assertThat(rightCaptor.firstValue[0].button.color.get()).isEqualTo(appliedOptions.topBar.rightButtonColor.get()) - assertThat(rightCaptor.firstValue[1].button.color.get()).isEqualTo(appliedOptions.topBar.rightButtonColor.get()) + val rightCaptor = argumentCaptor>() + verify(topBarController).mergeRightButtonsOptions(any(),rightCaptor.capture(), any()) + assertThat(rightCaptor.firstValue[0].color.get()).isEqualTo(appliedOptions.topBar.rightButtonColor.get()) + assertThat(rightCaptor.firstValue[1].color.get()).isEqualTo(appliedOptions.topBar.rightButtonColor.get()) assertThat(rightCaptor.firstValue[0]).isNotEqualTo(rightButton1) assertThat(rightCaptor.firstValue[1]).isNotEqualTo(rightButton2) - val leftCaptor = argumentCaptor>() - verify(topBarController).mergeLeftButtons(leftCaptor.capture(), any()) - assertThat(leftCaptor.firstValue[0].button.color.get()).isEqualTo(appliedOptions.topBar.leftButtonColor.get()) + val leftCaptor = argumentCaptor>() + verify(topBarController).mergeLeftButtonsOptions(any(),leftCaptor.capture(), any()) + assertThat(leftCaptor.firstValue[0].color.get()).isEqualTo(appliedOptions.topBar.leftButtonColor.get()) assertThat(leftCaptor.firstValue[0]).isNotEqualTo(leftButton) } @@ -816,15 +822,15 @@ class StackPresenterTest : BaseTest() { options2.topBar.buttons.left = ArrayList(listOf(leftButton)) uut.mergeChildOptions(options2, resolvedOptions, parent, child) - val rightCaptor = argumentCaptor>() - verify(topBarController).mergeRightButtons(rightCaptor.capture(), any()) - assertThat(rightCaptor.firstValue[0].button.color.get()).isEqualTo(resolvedOptions.topBar.rightButtonColor.get()) - assertThat(rightCaptor.firstValue[1].button.color.get()).isEqualTo(resolvedOptions.topBar.rightButtonColor.get()) + val rightCaptor = argumentCaptor>() + verify(topBarController).mergeRightButtonsOptions(any(),rightCaptor.capture(), any()) + assertThat(rightCaptor.firstValue[0].color.get()).isEqualTo(resolvedOptions.topBar.rightButtonColor.get()) + assertThat(rightCaptor.firstValue[1].color.get()).isEqualTo(resolvedOptions.topBar.rightButtonColor.get()) assertThat(rightCaptor.firstValue[0]).isNotEqualTo(rightButton1) assertThat(rightCaptor.firstValue[1]).isNotEqualTo(rightButton2) - val leftCaptor = argumentCaptor>() - verify(topBarController).mergeLeftButtons(leftCaptor.capture(), any()) - assertThat(leftCaptor.firstValue[0].button.color.get()).isEqualTo(resolvedOptions.topBar.leftButtonColor.get()) + val leftCaptor = argumentCaptor>() + verify(topBarController).mergeLeftButtonsOptions(any(),leftCaptor.capture(), any()) + assertThat(leftCaptor.firstValue[0].color.get()).isEqualTo(resolvedOptions.topBar.leftButtonColor.get()) assertThat(leftCaptor.firstValue[0]).isNotEqualTo(leftButton) } @@ -834,10 +840,10 @@ class StackPresenterTest : BaseTest() { options.topBar.buttons.right = ArrayList(listOf(textBtn1)) options.topBar.buttons.left = ArrayList(listOf(textBtn1)) uut.applyChildOptions(options, parent, child) - val rightCaptor = argumentCaptor>() - val leftCaptor = argumentCaptor>() - verify(topBarController).applyRightButtons(rightCaptor.capture()) - verify(topBarController).applyLeftButtons(leftCaptor.capture()) + val rightCaptor = argumentCaptor>() + val leftCaptor = argumentCaptor>() + verify(topBarController).applyRightButtonsOptions(any(),rightCaptor.capture(),any()) + verify(topBarController).applyLeftButtonsOptions(any(),leftCaptor.capture(),any()) assertThat(rightCaptor.firstValue.size).isOne() assertThat(leftCaptor.firstValue.size).isOne() } @@ -1022,6 +1028,7 @@ class StackPresenterTest : BaseTest() { assertThat((topBar.layoutParams as ViewGroup.MarginLayoutParams).topMargin).isEqualTo(10) } + private fun getCurrentChild()=parent.currentChild private fun assertTopBarOptions(options: Options, t: Int) { if (options.topBar.title.component.hasValue()) { verify(topBar, never()).title = any() diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TopBarControllerTest.kt b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TopBarControllerTest.kt index fc0baeb6fa4..00c337608fd 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TopBarControllerTest.kt +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TopBarControllerTest.kt @@ -6,24 +6,26 @@ import android.content.Context import android.view.View import com.nhaarman.mockitokotlin2.* import com.reactnativenavigation.BaseTest +import com.reactnativenavigation.fakes.IconResolverFake +import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock import com.reactnativenavigation.options.BackButton import com.reactnativenavigation.options.ButtonOptions +import com.reactnativenavigation.options.ComponentOptions import com.reactnativenavigation.options.Options import com.reactnativenavigation.options.params.Bool import com.reactnativenavigation.options.params.Text import com.reactnativenavigation.react.Constants import com.reactnativenavigation.react.ReactView -import com.reactnativenavigation.utils.CollectionUtils import com.reactnativenavigation.utils.TitleBarHelper import com.reactnativenavigation.utils.resetViewProperties import com.reactnativenavigation.viewcontrollers.stack.topbar.TopBarAnimator import com.reactnativenavigation.viewcontrollers.stack.topbar.TopBarController import com.reactnativenavigation.viewcontrollers.stack.topbar.button.ButtonController +import com.reactnativenavigation.viewcontrollers.stack.topbar.button.ButtonPresenter import com.reactnativenavigation.views.stack.StackLayout import com.reactnativenavigation.views.stack.topbar.TopBar import org.assertj.core.api.Java6Assertions.assertThat import org.junit.Test -import java.util.* class TopBarControllerTest : BaseTest() { private lateinit var uut: TopBarController @@ -34,10 +36,16 @@ class TopBarControllerTest : BaseTest() { private lateinit var textButton2: ButtonOptions private lateinit var componentButton: ButtonOptions private lateinit var animator: TopBarAnimator + private lateinit var leftButtonControllers: MutableMap + private lateinit var rightButtonControllers: MutableMap + + private val topBar: View get() = uut.view override fun beforeEach() { + leftButtonControllers= mutableMapOf() + rightButtonControllers= mutableMapOf() activity = newActivity() animator = spy(TopBarAnimator()) uut = createTopBarController() @@ -48,68 +56,111 @@ class TopBarControllerTest : BaseTest() { @Test fun setButton_setsTextButton() { - uut.applyRightButtons(rightButtons(textButton1)!!) - uut.applyLeftButtons(leftButton(leftButton)) + uut.applyRightButtonsOptions(rightButtonControllers, listOf(textButton1)){ + createButtonController(it) + } + uut.applyLeftButtonsOptions(leftButtonControllers, listOf(leftButton)){ + createButtonController(it) + } assertThat(uut.getRightButton(0).title.toString()).isEqualTo(textButton1.text.get()) } @Test fun setButton_setsCustomButton() { - uut.applyLeftButtons(leftButton(leftButton)) - uut.applyRightButtons(rightButtons(componentButton)!!) + uut.applyLeftButtonsOptions(leftButtonControllers, listOf(leftButton)){ + createButtonController(it) + } + uut.applyRightButtonsOptions(rightButtonControllers, listOf(componentButton)){ + createButtonController(it) + } val btnView = uut.getRightButton(0).actionView as ReactView assertThat(btnView.componentName).isEqualTo(componentButton.component.name.get()) } @Test fun applyRightButtons_emptyButtonsListClearsRightButtons() { - uut.applyLeftButtons(leftButton(leftButton)) - uut.applyRightButtons(rightButtons(componentButton, textButton1)!!) - uut.applyLeftButtons(leftButton(leftButton)) - uut.applyRightButtons(ArrayList()) + uut.applyLeftButtonsOptions(leftButtonControllers, listOf(leftButton)){ + createButtonController(it) + } + uut.applyRightButtonsOptions(rightButtonControllers, listOf(componentButton, textButton1)){ + createButtonController(it) + } + uut.applyLeftButtonsOptions(leftButtonControllers, listOf(leftButton)){ + createButtonController(it) + } + uut.applyRightButtonsOptions(rightButtonControllers, listOf()){ + createButtonController(it) + } assertThat(uut.rightButtonCount).isEqualTo(0) } @Test fun applyRightButtons_previousButtonsAreCleared() { - uut.applyRightButtons(rightButtons(textButton1, componentButton)!!) + uut.applyRightButtonsOptions(rightButtonControllers, listOf(textButton1, componentButton)){ + createButtonController(it) + } assertThat(uut.rightButtonCount).isEqualTo(2) - uut.applyRightButtons(rightButtons(textButton2)!!) + uut.applyRightButtonsOptions(rightButtonControllers, listOf(textButton2)){ + createButtonController(it) + } assertThat(uut.rightButtonCount).isEqualTo(1) } @Test fun applyRightButtons_buttonsAreAddedInReversedOrderToMatchOrderOnIOs() { - uut.applyLeftButtons(leftButton(leftButton)) - uut.applyRightButtons(rightButtons(textButton1, componentButton)!!) + uut.applyLeftButtonsOptions(leftButtonControllers, listOf(leftButton)){ + createButtonController(it) + } + uut.applyRightButtonsOptions(rightButtonControllers, listOf(textButton1, componentButton)){ + createButtonController(it) + } assertThat(uut.getRightButton(1).title.toString()).isEqualTo(textButton1.text.get()) } @Test fun applyRightButtons_componentButtonIsReapplied() { - val initialButtons = rightButtons(componentButton) - uut.applyRightButtons(initialButtons!!) + uut.applyRightButtonsOptions(rightButtonControllers, listOf( componentButton)){ + createButtonController(it) + } assertThat(uut.getRightButton(0).itemId).isEqualTo(componentButton.intId) - uut.applyRightButtons(rightButtons(textButton1)!!) + uut.applyRightButtonsOptions(rightButtonControllers, listOf( textButton1)){ + createButtonController(it) + } assertThat(uut.getRightButton(0).itemId).isEqualTo(textButton1.intId) - uut.applyRightButtons(initialButtons) + uut.applyRightButtonsOptions(rightButtonControllers, listOf( componentButton)){ + createButtonController(it) + } assertThat(uut.getRightButton(0).itemId).isEqualTo(componentButton.intId) } @Test - fun mergeRightButtons_componentButtonIsNotAddedIfAlreadyAddedToMenu() { - val initialButtons = rightButtons(componentButton) - uut.applyRightButtons(initialButtons!!) - uut.mergeRightButtons(initialButtons, emptyList()) + fun mergeRightButtonsOptions_componentButtonIsNotAddedIfAlreadyAddedToMenu() { + val controllers = mutableMapOf() + uut.applyRightButtonsOptions(controllers, listOf(componentButton)){ + createButtonController(it) + } + verify(controllers[componentButton.id]!!, times(1)).addToMenu(any(), any()) + uut.mergeRightButtonsOptions(controllers, listOf(componentButton.copy())){ + createButtonController(it) + } + verify(controllers[componentButton.id]!!, times(1)).addToMenu(any(), any()) } @Test fun setLeftButtons_emptyButtonsListClearsLeftButton() { - uut.applyLeftButtons(leftButton(leftButton)) - uut.applyRightButtons(rightButtons(componentButton)!!) + uut.applyLeftButtonsOptions(leftButtonControllers, listOf(leftButton)){ + createButtonController(it) + } + uut.applyRightButtonsOptions(rightButtonControllers, listOf( componentButton)){ + createButtonController(it) + } assertThat(uut.leftButtonCount).isNotZero() - uut.applyLeftButtons(emptyList()) - uut.applyRightButtons(rightButtons(textButton1)!!) + uut.applyLeftButtonsOptions(leftButtonControllers, listOf()){ + createButtonController(it) + } + uut.applyRightButtonsOptions(rightButtonControllers, listOf( textButton1)){ + createButtonController(it) + } assertThat(uut.leftButtonCount).isZero() } @@ -117,7 +168,9 @@ class TopBarControllerTest : BaseTest() { fun setLeftButtons_clearsBackButton() { uut.view.setBackButton(TitleBarHelper.createButtonController(activity, backButton)) assertThat(uut.view.navigationIcon).isNotNull() - uut.applyLeftButtons(leftButton(leftButton)) + uut.applyLeftButtonsOptions(leftButtonControllers, listOf(leftButton)){ + createButtonController(it) + } assertThat(uut.view.navigationIcon).isNull() } @@ -125,25 +178,35 @@ class TopBarControllerTest : BaseTest() { fun setLeftButtons_emptyButtonsListClearsBackButton() { uut.view.setBackButton(TitleBarHelper.createButtonController(activity, backButton)) assertThat(uut.view.navigationIcon).isNotNull() - uut.applyLeftButtons(emptyList()) + uut.applyLeftButtonsOptions(leftButtonControllers, listOf()){ + createButtonController(it) + } assertThat(uut.view.navigationIcon).isNull() } @Test fun mergeLeftButtons_clearsBackButton() { + val controllers = mutableMapOf() uut.view.setBackButton(TitleBarHelper.createButtonController(activity, backButton)) assertThat(uut.view.navigationIcon).isNotNull() - uut.mergeLeftButtons(emptyList(), leftButton(leftButton)) + uut.mergeLeftButtonsOptions(controllers, listOf(leftButton)){ + createButtonController(it) + } assertThat(uut.view.navigationIcon).isNull() } @Test fun mergeLeftButtons_emptyButtonsListClearsBackButton() { + val controllers = mutableMapOf() + uut.view.setBackButton(TitleBarHelper.createButtonController(activity, backButton)) assertThat(uut.view.navigationIcon).isNotNull() - val initialButtons = leftButton(leftButton) - uut.applyLeftButtons(initialButtons) - uut.mergeLeftButtons(initialButtons, emptyList()) + uut.applyLeftButtonsOptions(controllers, listOf(leftButton)){ + createButtonController(it) + } + uut.mergeLeftButtonsOptions(controllers, emptyList()){ + createButtonController(it) + } assertThat(uut.view.navigationIcon).isNull() } @@ -219,6 +282,127 @@ class TopBarControllerTest : BaseTest() { assertThat(result).isEqualTo(someAnimator) } + @Test + fun `mergeRightButtons - should add buttons`(){ + val controllers = spy(LinkedHashMap()) + val controller = spy(ButtonController(activity, ButtonPresenter(activity, textButton1, IconResolverFake(activity)), + textButton1, TitleBarButtonCreatorMock(), object : ButtonController.OnClickListener { + override fun onPress(button: ButtonOptions) { + + } + + })) + uut.mergeRightButtonsOptions(controllers, listOf(textButton1)) { + controller + } + assertThat(uut.rightButtonCount).isEqualTo(1) + verify(controllers, never()).remove(any()) + assertThat(controllers[textButton1.id]).isEqualTo(controller) + } + @Test + fun `mergeRightOptions - should destroy all buttons that was removed`(){ + val componentButton2 = componentButton.copy() + componentButton2.component = ComponentOptions().apply { + this.name = componentButton.component.name + this.componentId = Text("CustomNewComponent") + } + uut.mergeRightButtonsOptions(rightButtonControllers, listOf(textButton1, textButton2, componentButton)) { + createButtonController(it) + } + val removedControllers = mutableMapOf().apply { + putAll(rightButtonControllers) + } + uut.mergeRightButtonsOptions(rightButtonControllers, listOf(componentButton2)) { + createButtonController(it) + } + verify(removedControllers[textButton1.id]!!, times(1)).destroy() + verify(removedControllers[textButton2.id]!!, times(1)).destroy() + verify(removedControllers[componentButton.id]!!, times(1)).destroy() + } + @Test + fun `mergeRightButtons - should remove all and re-add buttons in case of reorder, without destroy`(){ + uut.mergeRightButtonsOptions(rightButtonControllers, listOf(textButton1, textButton2)) { + createButtonController(it) + } + assertThat(uut.getRightButton(1).itemId ).isEqualTo(textButton1.intId) + assertThat(uut.getRightButton(0).itemId ).isEqualTo(textButton2.intId) + val removedControllers = mutableMapOf().apply { putAll(rightButtonControllers) } + uut.mergeRightButtonsOptions(rightButtonControllers, listOf(textButton2.copy(), textButton1.copy())) { + createButtonController(it) + } + assertThat(uut.getRightButton(1).itemId ).isEqualTo(textButton2.intId) + assertThat(uut.getRightButton(0).itemId ).isEqualTo(textButton1.intId) + + verify(removedControllers[textButton1.id]!!, never()).destroy() + verify(removedControllers[textButton2.id]!!, never()).destroy() + + verify(rightButtonControllers[textButton1.id]!!, times(1)).addToMenu(any(), any()) + verify(rightButtonControllers[textButton2.id]!!, times(1)).addToMenu(any(), any()) + } + @Test + fun `mergeRightButtons - should rebuild menu when adding menu items`(){ + val controllers = spy(LinkedHashMap()) + uut.mergeRightButtonsOptions(controllers, listOf(textButton1)) { + createButtonController(it) + } + assertThat(uut.rightButtonCount).isEqualTo(1) + + uut.mergeRightButtonsOptions(controllers, listOf(textButton1, textButton2)) { + createButtonController(it) + } + assertThat(uut.rightButtonCount).isEqualTo(2) + verify(controllers, times(1)).remove(any()) + verify(controllers[textButton1.id]!!, times(1)).addToMenu(any(), any()) + } + @Test + fun `mergeRightButtons - should modify changed buttons`(){ + val controllers = spy(LinkedHashMap()) + uut.mergeRightButtonsOptions(controllers, listOf(textButton1.apply { + this.enabled = Bool(true) + })) { + createButtonController(it) + } + assertThat(uut.rightButtonCount).isEqualTo(1) + verify(controllers[textButton1.id]!!, times(1)).addToMenu(any(), any()) + + uut.mergeRightButtonsOptions(controllers, listOf(textButton1.copy().apply { this.enabled= Bool(false) })) { + createButtonController(it) + } + verify(controllers, never()).remove(any()) + verify(controllers[textButton1.id]!!, times(1)).mergeButtonOptions(any(), any()) + verify(controllers[textButton1.id]!!, times(1)).addToMenu(any(), any()) + verify(controllers[textButton1.id]!!, never()).destroy() + } + + fun `mergeRightButtons - reorder of same menu items should rebuild menu`(){ + val controllers = spy(LinkedHashMap()) + uut.mergeRightButtonsOptions(controllers, listOf(textButton1, textButton2)) { + createButtonController(it) + } + assertThat(uut.rightButtonCount).isEqualTo(1) + verify(controllers[textButton1.id]!!, times(1)).addToMenu(any(), any()) + verify(controllers[textButton2.id]!!, times(1)).addToMenu(any(), any()) + + uut.mergeRightButtonsOptions(controllers, listOf(textButton2.copy(), textButton1.copy())) { + createButtonController(it) + } + verify(controllers[textButton1.id]!!, never()).mergeButtonOptions(any(), any()) + verify(controllers[textButton2.id]!!, never()).mergeButtonOptions(any(), any()) + verify(controllers[textButton1.id]!!, times(2)).addToMenu(any(), any()) + verify(controllers[textButton2.id]!!, times(2)).addToMenu(any(), any()) + verify(controllers[textButton1.id]!!, times(1)).destroy() + verify(controllers[textButton2.id]!!, times(1)).destroy() + } + + private fun createButtonController(it: ButtonOptions) = + spy(ButtonController(activity, ButtonPresenter(activity, it, IconResolverFake(activity)), + it, TitleBarButtonCreatorMock(), object : ButtonController.OnClickListener { + override fun onPress(button: ButtonOptions) { + + } + + })) + private fun createButtons() { leftButton = ButtonOptions() leftButton.id = Constants.BACK_BUTTON_ID @@ -238,17 +422,10 @@ class TopBarControllerTest : BaseTest() { return button } - private fun leftButton(leftButton: ButtonOptions): List { - return listOf(TitleBarHelper.createButtonController(activity, leftButton)) - } - - private fun rightButtons(vararg buttons: ButtonOptions): List? { - return CollectionUtils.map(listOf(*buttons)) { button: ButtonOptions? -> TitleBarHelper.createButtonController(activity, button) } - } - private fun createTopBarController() = spy(object : TopBarController(animator) { override fun createTopBar(context: Context, stackLayout: StackLayout): TopBar { return spy(super.createTopBar(context, stackLayout)) } }) + } \ No newline at end of file diff --git a/lib/ios/RNNAssert.h b/lib/ios/RNNAssert.h index 1cc79aa8d51..9d4db01738c 100644 --- a/lib/ios/RNNAssert.h +++ b/lib/ios/RNNAssert.h @@ -12,8 +12,8 @@ extern BOOL RNNIsMainQueue(void); if ((condition) == 0) { \ if (RNN_NSASSERT) { \ [[NSAssertionHandler currentHandler] \ - handleFailureInFunction:(NSString * _Nonnull) @(__func__) \ - file:(NSString * _Nonnull) @(__FILE__) \ + handleFailureInFunction:(NSString *_Nonnull)@(__func__) \ + file:(NSString *_Nonnull)@(__FILE__) \ lineNumber:__LINE__ \ description:__VA_ARGS__]; \ } \ diff --git a/lib/ios/RNNBottomTabOptions.m b/lib/ios/RNNBottomTabOptions.m index 0f24da1d61a..a164e64aae4 100644 --- a/lib/ios/RNNBottomTabOptions.m +++ b/lib/ios/RNNBottomTabOptions.m @@ -84,7 +84,8 @@ - (BOOL)hasValue { self.testID.hasValue || self.icon.hasValue || self.selectedIcon.hasValue || self.iconColor.hasValue || self.selectedIconColor.hasValue || self.selectedTextColor.hasValue || self.iconInsets.hasValue || self.textColor.hasValue || - self.visible.hasValue || self.selectTabOnPress.hasValue || self.sfSymbol.hasValue || self.sfSelectedSymbol.hasValue; + self.visible.hasValue || self.selectTabOnPress.hasValue || self.sfSymbol.hasValue || + self.sfSelectedSymbol.hasValue; } @end diff --git a/lib/ios/RNNButtonBuilder.m b/lib/ios/RNNButtonBuilder.m index ff2cad4fed8..24e3cb3589b 100644 --- a/lib/ios/RNNButtonBuilder.m +++ b/lib/ios/RNNButtonBuilder.m @@ -31,9 +31,9 @@ - (RNNUIBarButtonItem *)build:(RNNButtonOptions *)button return [[RNNUIBarButtonItem alloc] initCustomIcon:button iconCreator:_iconCreator onPress:onPress]; - } else if (button.sfSymbol.hasValue) { - return [[RNNUIBarButtonItem alloc] initWithSFSymbol:button onPress:onPress]; - } else if (button.icon.hasValue) { + } else if (button.sfSymbol.hasValue) { + return [[RNNUIBarButtonItem alloc] initWithSFSymbol:button onPress:onPress]; + } else if (button.icon.hasValue) { return [[RNNUIBarButtonItem alloc] initWithIcon:button onPress:onPress]; } else if (button.text.hasValue) { return [[RNNUIBarButtonItem alloc] initWithTitle:button onPress:onPress]; diff --git a/lib/ios/RNNButtonOptions.m b/lib/ios/RNNButtonOptions.m index 8f4d4ab69fa..a3119131d8d 100644 --- a/lib/ios/RNNButtonOptions.m +++ b/lib/ios/RNNButtonOptions.m @@ -11,7 +11,7 @@ - (instancetype)initWithDict:(NSDictionary *)dict { self.fontWeight = [TextParser parse:dict key:@"fontWeight"]; self.fontSize = [NumberParser parse:dict key:@"fontSize"]; self.text = [TextParser parse:dict key:@"text"]; - self.sfSymbol = [TextParser parse:dict key:@"sfSymbol"]; + self.sfSymbol = [TextParser parse:dict key:@"sfSymbol"]; self.testID = [TextParser parse:dict key:@"testID"]; self.accessibilityLabel = [TextParser parse:dict key:@"accessibilityLabel"]; self.color = [ColorParser parse:dict key:@"color"]; @@ -40,7 +40,7 @@ - (RNNButtonOptions *)copy { newOptions.color = self.color.copy; newOptions.disabledColor = self.disabledColor.copy; newOptions.icon = self.icon.copy; - newOptions.sfSymbol = self.sfSymbol.copy; + newOptions.sfSymbol = self.sfSymbol.copy; newOptions.iconInsets = self.iconInsets.copy; newOptions.enabled = self.enabled.copy; newOptions.selectTabOnPress = self.selectTabOnPress.copy; @@ -73,8 +73,8 @@ - (void)mergeOptions:(RNNButtonOptions *)options { self.disabledColor = options.disabledColor; if (options.icon.hasValue) self.icon = options.icon; - if (options.sfSymbol.hasValue) - self.sfSymbol = options.sfSymbol; + if (options.sfSymbol.hasValue) + self.sfSymbol = options.sfSymbol; if (options.enabled.hasValue) { self.enabled = options.enabled; [self.iconBackground setEnabled:self.enabled]; diff --git a/lib/ios/RNNCommandsHandler.m b/lib/ios/RNNCommandsHandler.m index 5eb5078892d..3c6c67fab21 100644 --- a/lib/ios/RNNCommandsHandler.m +++ b/lib/ios/RNNCommandsHandler.m @@ -82,10 +82,7 @@ - (void)setRoot:(NSDictionary *)layout } } - [_modalManager dismissAllModalsAnimated:NO - completion:^{ - - }]; + [_modalManager reset]; UIViewController *vc = [_controllerFactory createLayout:layout[@"root"]]; [_layoutManager addPendingViewController:vc]; diff --git a/lib/ios/RNNDotIndicatorPresenter.m b/lib/ios/RNNDotIndicatorPresenter.m index 6869d071e20..635c1151f2a 100644 --- a/lib/ios/RNNDotIndicatorPresenter.m +++ b/lib/ios/RNNDotIndicatorPresenter.m @@ -92,7 +92,8 @@ - (BOOL)currentIndicatorEquals:(UIViewController *)child options:(DotIndicatorOp return NO; UIView *currentIndicator = [self getCurrentIndicator:child]; - return [[currentIndicator backgroundColor] isEqual:[options.color withDefault:[UIColor redColor]]]; + return + [[currentIndicator backgroundColor] isEqual:[options.color withDefault:[UIColor redColor]]]; } - (UIView *)getCurrentIndicator:(UIViewController *)child { diff --git a/lib/ios/RNNModalManager.h b/lib/ios/RNNModalManager.h index 609e6bb8065..07b431b837d 100644 --- a/lib/ios/RNNModalManager.h +++ b/lib/ios/RNNModalManager.h @@ -24,4 +24,6 @@ typedef void (^RNNTransitionRejectionBlock)(NSString *_Nonnull code, NSString *_ - (void)dismissAllModalsAnimated:(BOOL)animated completion:(void (^__nullable)(void))completion; - (void)dismissAllModalsSynchronosly; +- (void)reset; + @end diff --git a/lib/ios/RNNModalManager.m b/lib/ios/RNNModalManager.m index c50ef61983d..cf6d6ae4201 100644 --- a/lib/ios/RNNModalManager.m +++ b/lib/ios/RNNModalManager.m @@ -36,12 +36,14 @@ - (void)connectModalHostViewManager:(RCTModalHostViewManager *)modalHostViewMana modalHostViewManager.presentationBlock = ^(UIViewController *reactViewController, UIViewController *viewController, BOOL animated, dispatch_block_t completionBlock) { - [self showModal:viewController - animated:animated - completion:^(NSString *_Nonnull componentId) { - if (completionBlock) - completionBlock(); - }]; + if (reactViewController.presentedViewController != viewController) { + [self showModal:viewController + animated:animated + completion:^(NSString *_Nonnull componentId) { + if (completionBlock) + completionBlock(); + }]; + } }; modalHostViewManager.dismissalBlock = @@ -143,6 +145,11 @@ - (void)dismissAllModalsSynchronosly { } } +- (void)reset { + [_presentedModals removeAllObjects]; + [_pendingModalIdsToDismiss removeAllObjects]; +} + #pragma mark - private - (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion diff --git a/lib/ios/RNNSegmentedControl.h b/lib/ios/RNNSegmentedControl.h index 41771612a73..64ddd8d14d4 100644 --- a/lib/ios/RNNSegmentedControl.h +++ b/lib/ios/RNNSegmentedControl.h @@ -1,5 +1,5 @@ -#import #import +#import @interface RNNSegmentedControl : HMSegmentedControl diff --git a/lib/ios/RNNTabBarItemCreator.m b/lib/ios/RNNTabBarItemCreator.m index b795501f568..140c7be8961 100644 --- a/lib/ios/RNNTabBarItemCreator.m +++ b/lib/ios/RNNTabBarItemCreator.m @@ -18,11 +18,12 @@ - (UITabBarItem *)createTabBarItem:(RNNBottomTabOptions *)bottomTabOptions if (@available(iOS 13.0, *)) { if (bottomTabOptions.sfSymbol.hasValue) { - icon = [UIImage systemImageNamed: [bottomTabOptions.sfSymbol withDefault:nil]]; + icon = [UIImage systemImageNamed:[bottomTabOptions.sfSymbol withDefault:nil]]; } if (bottomTabOptions.sfSelectedSymbol.hasValue) { - selectedIcon = [UIImage systemImageNamed: [bottomTabOptions.sfSelectedSymbol withDefault:nil]]; + selectedIcon = + [UIImage systemImageNamed:[bottomTabOptions.sfSelectedSymbol withDefault:nil]]; } } diff --git a/lib/ios/RNNUIBarButtonItem.h b/lib/ios/RNNUIBarButtonItem.h index bbffeea3222..47ffde69581 100644 --- a/lib/ios/RNNUIBarButtonItem.h +++ b/lib/ios/RNNUIBarButtonItem.h @@ -15,7 +15,7 @@ typedef void (^RNNButtonPressCallback)(NSString *buttonId); iconCreator:(RNNIconCreator *)iconCreator onPress:(RNNButtonPressCallback)onPress; - (instancetype)initWithSFSymbol:(RNNButtonOptions *)buttonOptions - onPress:(RNNButtonPressCallback)onPress; + onPress:(RNNButtonPressCallback)onPress; - (instancetype)initWithIcon:(RNNButtonOptions *)buttonOptions onPress:(RNNButtonPressCallback)onPress; - (instancetype)initWithTitle:(RNNButtonOptions *)buttonOptions diff --git a/lib/ios/RNNUIBarButtonItem.m b/lib/ios/RNNUIBarButtonItem.m index 4f177c2cde0..c49cb2fb8df 100644 --- a/lib/ios/RNNUIBarButtonItem.m +++ b/lib/ios/RNNUIBarButtonItem.m @@ -21,12 +21,12 @@ - (instancetype)init { } - (instancetype)initWithSFSymbol:(RNNButtonOptions *)buttonOptions - onPress:(RNNButtonPressCallback)onPress { + onPress:(RNNButtonPressCallback)onPress { UIImage *iconImage = [UIImage alloc]; - if (@available(iOS 13.0, *)) { + if (@available(iOS 13.0, *)) { iconImage = [UIImage systemImageNamed:[buttonOptions.sfSymbol withDefault:nil]]; - } + } self = [super initWithImage:iconImage style:UIBarButtonItemStylePlain diff --git a/lib/ios/TopBarPresenter.m b/lib/ios/TopBarPresenter.m index a5b5ca1bcc7..2c9c35fe6a6 100644 --- a/lib/ios/TopBarPresenter.m +++ b/lib/ios/TopBarPresenter.m @@ -160,17 +160,21 @@ - (void)setBackButtonOptions:(RNNBackButtonOptions *)backButtonOptions { UIBarButtonItem *backItem = [[RNNUIBarBackButtonItem alloc] initWithOptions:backButtonOptions]; UINavigationItem *previousNavigationItem = previousViewControllerInStack.navigationItem; - if (@available(iOS 13.0, *)) { UIImage *sfSymbol = [UIImage systemImageNamed:[backButtonOptions.sfSymbol withDefault:nil]]; if (backButtonOptions.sfSymbol.hasValue) { - icon = color ? [sfSymbol imageWithTintColor:color renderingMode:UIImageRenderingModeAlwaysOriginal] + icon = color ? [sfSymbol imageWithTintColor:color + renderingMode:UIImageRenderingModeAlwaysOriginal] : [sfSymbol imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; } else { - icon = color ? [[icon withTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] : icon; + icon = color ? [[icon withTintColor:color] + imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] + : icon; } } else { - icon = color ? [[icon withTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] : icon; + icon = color ? [[icon withTintColor:color] + imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] + : icon; } [self setBackIndicatorImage:icon withColor:color]; diff --git a/lib/src/commands/Commands.test.ts b/lib/src/commands/Commands.test.ts index 7f91792ba16..f1be4567865 100644 --- a/lib/src/commands/Commands.test.ts +++ b/lib/src/commands/Commands.test.ts @@ -176,7 +176,7 @@ describe('Commands', () => { it('retains passProps properties identity', () => { const obj = { some: 'content' }; uut.setRoot({ root: { component: { name: 'com.example.MyScreen', passProps: { obj } } } }); - const args = capture(mockedStore.updateProps).last(); + const args = capture(mockedStore.setPendingProps).last(); expect(args[1].obj).toBe(obj); }); }); @@ -293,7 +293,7 @@ describe('Commands', () => { it('retains passProps properties identity', () => { const obj = { some: 'content' }; uut.showModal({ component: { name: 'com.example.MyScreen', passProps: { obj } } }); - const args = capture(mockedStore.updateProps).last(); + const args = capture(mockedStore.setPendingProps).last(); expect(args[1].obj).toBe(obj); }); }); @@ -380,7 +380,7 @@ describe('Commands', () => { uut.push('theComponentId', { component: { name: 'com.example.MyScreen', passProps: { obj } }, }); - const args = capture(mockedStore.updateProps).last(); + const args = capture(mockedStore.setPendingProps).last(); expect(args[1].obj).toBe(obj); }); }); @@ -477,7 +477,7 @@ describe('Commands', () => { uut.setStackRoot('theComponentId', [ { component: { name: 'com.example.MyScreen', passProps: { obj } } }, ]); - const args = capture(mockedStore.updateProps).last(); + const args = capture(mockedStore.setPendingProps).last(); expect(args[1].obj).toBe(obj); }); }); @@ -521,7 +521,7 @@ describe('Commands', () => { it('retains passProps properties identity', () => { const obj = { some: 'content' }; uut.showOverlay({ component: { name: 'com.example.MyScreen', passProps: { obj } } }); - const args = capture(mockedStore.updateProps).last(); + const args = capture(mockedStore.setPendingProps).last(); expect(args[1].obj).toBe(obj); }); }); diff --git a/lib/src/commands/LayoutTreeCrawler.test.ts b/lib/src/commands/LayoutTreeCrawler.test.ts index d34ec14115d..2e217662289 100644 --- a/lib/src/commands/LayoutTreeCrawler.test.ts +++ b/lib/src/commands/LayoutTreeCrawler.test.ts @@ -31,7 +31,7 @@ describe('LayoutTreeCrawler', () => { data: {}, }; uut.crawl(node, CommandName.SetRoot); - verify(mockedStore.updateProps('testId', deepEqual({ myProp: 123 }))).called(); + verify(mockedStore.setPendingProps('testId', deepEqual({ myProp: 123 }))).called(); }); it('Components: must contain data name', () => { diff --git a/lib/src/commands/LayoutTreeCrawler.ts b/lib/src/commands/LayoutTreeCrawler.ts index 907eb512eef..3aa60a026a7 100644 --- a/lib/src/commands/LayoutTreeCrawler.ts +++ b/lib/src/commands/LayoutTreeCrawler.ts @@ -35,7 +35,7 @@ export class LayoutTreeCrawler { } private savePropsToStore(node: LayoutNode) { - this.store.updateProps(node.id, node.data.passProps); + this.store.setPendingProps(node.id, node.data.passProps); } private assertComponentDataName(component: LayoutNode) { diff --git a/lib/src/commands/OptionsProcessor.test.ts b/lib/src/commands/OptionsProcessor.test.ts index 31d0c2edb12..55fd667113b 100644 --- a/lib/src/commands/OptionsProcessor.test.ts +++ b/lib/src/commands/OptionsProcessor.test.ts @@ -638,7 +638,7 @@ describe('navigation options', () => { uut.processOptions(options, CommandName.SetRoot); - verify(mockedStore.updateProps('CustomComponent1', passProps)).called(); + verify(mockedStore.setPendingProps('CustomComponent1', passProps)).called(); }); it('generates componentId for component id was not passed', () => { @@ -667,7 +667,7 @@ describe('navigation options', () => { uut.processOptions(options, CommandName.SetRoot); - verify(mockedStore.updateProps('1', passProps)).called(); + verify(mockedStore.setPendingProps('1', passProps)).called(); }); it('do not touch passProps when id for button is missing', () => { diff --git a/lib/src/commands/OptionsProcessor.ts b/lib/src/commands/OptionsProcessor.ts index 0985b92a4f4..c66dcc01e0b 100644 --- a/lib/src/commands/OptionsProcessor.ts +++ b/lib/src/commands/OptionsProcessor.ts @@ -200,7 +200,7 @@ export class OptionsProcessor { if (endsWith(key, 'Buttons')) { forEach(value, (button) => { if (button.passProps && button.id) { - this.store.updateProps(button.id, button.passProps); + this.store.setPendingProps(button.id, button.passProps); button.passProps = undefined; } }); @@ -212,7 +212,7 @@ export class OptionsProcessor { value.componentId = value.id ? value.id : this.uniqueIdProvider.generate('CustomComponent'); this.store.ensureClassForName(value.name); if (value.passProps) { - this.store.updateProps(value.componentId, value.passProps); + this.store.setPendingProps(value.componentId, value.passProps); } options[key].passProps = undefined; } diff --git a/lib/src/components/Store.test.ts b/lib/src/components/Store.test.ts index 323c16cd31d..5f5bc21a90d 100644 --- a/lib/src/components/Store.test.ts +++ b/lib/src/components/Store.test.ts @@ -102,4 +102,20 @@ describe('Store', () => { expect(uut.getPropsForId('component1')).toEqual({ foo: 'foo2', bar: 'bar' }); }); + + it('clearing component props should not clear pending props', () => { + uut.updateProps('component1', { foo: 'foo2' }); + uut.setPendingProps('component1', { foo: 'foo', bar: 'bar' }); + uut.clearComponent('component1'); + + expect(uut.getPropsForId('component1')).toEqual({ foo: 'foo', bar: 'bar' }); + }); + + it('should clear pending props after consumed', () => { + uut.setPendingProps('component1', { foo: 'foo', bar: 'bar' }); + uut.getPropsForId('component1'); + uut.clearComponent('component1'); + + expect(uut.getPropsForId('component1')).toEqual({}); + }); }); diff --git a/lib/src/components/Store.ts b/lib/src/components/Store.ts index 416f6dea5d2..08cfc07d0d6 100644 --- a/lib/src/components/Store.ts +++ b/lib/src/components/Store.ts @@ -5,6 +5,7 @@ import { IWrappedComponent } from './ComponentWrapper'; export class Store { private componentsByName: Record = {}; private propsById: Record = {}; + private pendingPropsById: Record = {}; private componentsInstancesById: Record = {}; private wrappedComponents: Record> = {}; private lazyRegistratorFn: ((lazyComponentRequest: string | number) => void) | undefined; @@ -18,7 +19,15 @@ export class Store { } } + setPendingProps(componentId: string, newProps: any) { + this.pendingPropsById[componentId] = newProps; + } + getPropsForId(componentId: string) { + if (this.pendingPropsById[componentId]) { + this.propsById[componentId] = this.pendingPropsById[componentId]; + delete this.pendingPropsById[componentId]; + } return this.propsById[componentId] || {}; } diff --git a/package.json b/package.json index 97dd731d238..e56ce67ec32 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "metro-react-native-babel-preset": "0.66.2", "prettier": "2.1.2", "react": "17.0.2", - "react-native": "0.66.0", + "react-native": "0.66.2", "react-native-fast-image": "^8.3.4", "react-native-gesture-handler": "^1.6.1", "react-native-reanimated": "2.3.0-beta.2", diff --git a/playground/src/app.ts b/playground/src/app.ts index 7713962f2bc..e00ec855bd5 100644 --- a/playground/src/app.ts +++ b/playground/src/app.ts @@ -31,6 +31,11 @@ function setRoot() { Navigation.setRoot({ root: { bottomTabs: { + options: { + bottomTabs: { + testID: testIDs.MAIN_BOTTOM_TABS, + }, + }, children: [ { stack: { diff --git a/playground/src/screens/KeyboardScreen.tsx b/playground/src/screens/KeyboardScreen.tsx index 820570dd3e7..165f20e3568 100644 --- a/playground/src/screens/KeyboardScreen.tsx +++ b/playground/src/screens/KeyboardScreen.tsx @@ -1,10 +1,21 @@ import React from 'react'; -import { View, ScrollView, Dimensions, StyleSheet, Image, Text, TextInput } from 'react-native'; -import { Navigation, NavigationComponentProps } from 'react-native-navigation'; - +import { View, ScrollView, Dimensions, StyleSheet, Image, TextInput, Text } from 'react-native'; +import { + NavigationComponentProps, + NavigationComponent, + ComponentDidAppearEvent, +} from 'react-native-navigation'; +import Navigation from '../services/Navigation'; +import Button from '../components/Button'; +import Screens from './Screens'; +import testIDs from '../testIDs'; +import { stack } from '../commons/Layouts'; const screenWidth = Dimensions.get('window').width; - -export default class KeyboardScreen extends React.Component { +const KEYBOARD_LABEL = 'Keyboard Demo'; +interface Props extends NavigationComponentProps { + title?: string; +} +export default class KeyboardScreen extends NavigationComponent { static options() { return { bottomTabs: { @@ -14,24 +25,94 @@ export default class KeyboardScreen extends React.Component - Keyboard e2e - - - {/* {LOREM_IPSUM} */} + +