diff --git a/e2e/Modals.test.js b/e2e/Modals.test.js index 5b5b37685fa..9379f324453 100644 --- a/e2e/Modals.test.js +++ b/e2e/Modals.test.js @@ -155,10 +155,54 @@ describe('modal', () => { await expect(elementByLabel('dismissModal promise resolved with: UniqueStackId')).toBeVisible(); }); - it('dismiss previous react-native modal', async () => { - await elementById(TestIDs.TOGGLE_REACT_NATIVE_MODAL).tap(); - await elementById(TestIDs.SHOW_MODAL_AND_DISMISS_REACT_NATIVE_MODAL).tap(); + it.e2e('should show declated modal', async () => { + await elementById(TestIDs.TOGGLE_REACT_DECLARED_MODAL).tap(); + await expect(elementByLabel("Dismiss declared Modal")).toBeVisible(); await elementById(TestIDs.DISMISS_MODAL_BTN).tap(); await expect(elementById(TestIDs.MODAL_SCREEN_HEADER)).toBeVisible(); }); + + it.e2e('should show and dismiss multiple modals including declared modal', async () => { + await elementById(TestIDs.TOGGLE_REACT_DECLARED_MODAL).tap(); + await elementById(TestIDs.SHOW_MODAL_FROM_DECLARED_BUTTON).tap(); + await expect(elementByLabel("Toggle declared modal")).toBeVisible(); + await elementById(TestIDs.TOGGLE_REACT_DECLARED_MODAL).tap(); + await elementById(TestIDs.DISMISS_MODAL_BTN).tap(); + await elementById(TestIDs.DISMISS_MODAL_BTN).tap(); + await expect(elementByLabel("Dismiss declared Modal")).toBeVisible(); + await elementById(TestIDs.DISMISS_MODAL_BTN).tap(); + + await expect(elementById(TestIDs.MODAL_SCREEN_HEADER)).toBeVisible(); + }); + + it.e2e('overlay should be on top of all modals', async () => { + await elementById(TestIDs.TOGGLE_REACT_DECLARED_MODAL).tap(); + await elementById(TestIDs.OVERLAY_BTN).tap(); + await expect(elementByLabel("Dismiss declared Modal")).toBeVisible(); + await expect(elementById(TestIDs.DISMISS_ALL_OVERLAYS_BUTTON)).toBeVisible(); + + await elementById(TestIDs.SHOW_MODAL_FROM_DECLARED_BUTTON).tap(); + await expect(elementByLabel("Modal Lifecycle")).toBeVisible(); + + await elementById(TestIDs.DISMISS_MODAL_BTN).tap(); + await elementById(TestIDs.DISMISS_MODAL_BTN).tap(); + + await elementById(TestIDs.DISMISS_ALL_OVERLAYS_BUTTON).tap(); + }); + + + + it.e2e(':android: should handle back properly', async () => { + await elementById(TestIDs.TOGGLE_REACT_DECLARED_MODAL).tap(); + await elementById(TestIDs.SHOW_MODAL_FROM_DECLARED_BUTTON).tap(); + await expect(elementByLabel("Toggle declared modal")).toBeVisible(); + + Android.pressBack(); + + await expect(elementByLabel("Dismiss declared Modal")).toBeVisible(); + + Android.pressBack(); + + await expect(elementByLabel("Toggle declared modal")).toBeVisible(); + }); }); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationPackage.java b/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationPackage.java deleted file mode 100644 index eaf79c8df9b..00000000000 --- a/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationPackage.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.reactnativenavigation.react; - -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; -import com.reactnativenavigation.options.LayoutFactory; - -import java.util.Collections; -import java.util.List; - -import androidx.annotation.NonNull; - -import static java.util.Collections.singletonList; - -public class NavigationPackage implements ReactPackage { - - private ReactNativeHost reactNativeHost; - - public NavigationPackage(final ReactNativeHost reactNativeHost) { - this.reactNativeHost = reactNativeHost; - } - - @NonNull - @Override - public List createNativeModules(@NonNull ReactApplicationContext reactContext) { - return singletonList(new NavigationModule( - reactContext, - reactNativeHost.getReactInstanceManager(), - new LayoutFactory(reactNativeHost.getReactInstanceManager()) - ) - ); - } - - @NonNull - @Override - public List createViewManagers(@NonNull ReactApplicationContext reactContext) { - return Collections.emptyList(); - } -} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationPackage.kt b/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationPackage.kt new file mode 100644 index 00000000000..2f8f3d5e70b --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationPackage.kt @@ -0,0 +1,27 @@ +package com.reactnativenavigation.react + +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager +import com.reactnativenavigation.NavigationActivity +import com.reactnativenavigation.options.LayoutFactory +import com.reactnativenavigation.react.modal.ModalViewManager + +class NavigationPackage(private val reactNativeHost: ReactNativeHost) : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf( + NavigationModule( + reactContext, + reactNativeHost.reactInstanceManager, + LayoutFactory(reactNativeHost.reactInstanceManager) + ) + ) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + + return listOf(ModalViewManager((reactContext.currentActivity as NavigationActivity).navigator)) + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationReactInitializer.java b/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationReactInitializer.java index c289d6f6eb9..4aea1aecb7f 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationReactInitializer.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationReactInitializer.java @@ -9,67 +9,67 @@ public class NavigationReactInitializer implements ReactInstanceManager.ReactInstanceEventListener { - private final ReactInstanceManager reactInstanceManager; - private final DevPermissionRequest devPermissionRequest; - private boolean waitingForAppLaunchEvent = true; - private boolean isActivityReadyForUi = false; + private final ReactInstanceManager reactInstanceManager; + private final DevPermissionRequest devPermissionRequest; + private boolean waitingForAppLaunchEvent = true; + private boolean isActivityReadyForUi = false; - NavigationReactInitializer(ReactInstanceManager reactInstanceManager, boolean isDebug) { - this.reactInstanceManager = reactInstanceManager; - this.devPermissionRequest = new DevPermissionRequest(isDebug); - } + NavigationReactInitializer(ReactInstanceManager reactInstanceManager, boolean isDebug) { + this.reactInstanceManager = reactInstanceManager; + this.devPermissionRequest = new DevPermissionRequest(isDebug); + } - void onActivityCreated() { - reactInstanceManager.addReactInstanceEventListener(this); - waitingForAppLaunchEvent = true; - } + void onActivityCreated() { + reactInstanceManager.addReactInstanceEventListener(this); + waitingForAppLaunchEvent = true; + } - void onActivityResumed(NavigationActivity activity) { - if (devPermissionRequest.shouldAskPermission(activity)) { - devPermissionRequest.askPermission(activity); - } else { - reactInstanceManager.onHostResume(activity, activity); + void onActivityResumed(NavigationActivity activity) { + if (devPermissionRequest.shouldAskPermission(activity)) { + devPermissionRequest.askPermission(activity); + } else { + reactInstanceManager.onHostResume(activity, activity); isActivityReadyForUi = true; - prepareReactApp(); - } - } + prepareReactApp(); + } + } - void onActivityPaused(NavigationActivity activity) { + void onActivityPaused(NavigationActivity activity) { isActivityReadyForUi = false; - if (reactInstanceManager.hasStartedCreatingInitialContext()) { - reactInstanceManager.onHostPause(activity); - } - } + if (reactInstanceManager.hasStartedCreatingInitialContext()) { + reactInstanceManager.onHostPause(activity); + } + } - void onActivityDestroyed(NavigationActivity activity) { - reactInstanceManager.removeReactInstanceEventListener(this); - if (reactInstanceManager.hasStartedCreatingInitialContext()) { - reactInstanceManager.onHostDestroy(activity); - } - } + void onActivityDestroyed(NavigationActivity activity) { + reactInstanceManager.removeReactInstanceEventListener(this); + if (reactInstanceManager.hasStartedCreatingInitialContext()) { + reactInstanceManager.onHostDestroy(activity); + } + } - private void prepareReactApp() { - if (shouldCreateContext()) { - reactInstanceManager.createReactContextInBackground(); - } else if (waitingForAppLaunchEvent) { + private void prepareReactApp() { + if (shouldCreateContext()) { + reactInstanceManager.createReactContextInBackground(); + } else if (waitingForAppLaunchEvent) { if (reactInstanceManager.getCurrentReactContext() != null) { - emitAppLaunched(reactInstanceManager.getCurrentReactContext()); + emitAppLaunched(reactInstanceManager.getCurrentReactContext()); } - } - } + } + } - private void emitAppLaunched(@NonNull ReactContext context) { - if (!isActivityReadyForUi) return; - waitingForAppLaunchEvent = false; - new EventEmitter(context).appLaunched(); - } + private void emitAppLaunched(@NonNull ReactContext context) { + if (!isActivityReadyForUi) return; + waitingForAppLaunchEvent = false; + new EventEmitter(context).appLaunched(); + } - private boolean shouldCreateContext() { - return !reactInstanceManager.hasStartedCreatingInitialContext(); - } + private boolean shouldCreateContext() { + return !reactInstanceManager.hasStartedCreatingInitialContext(); + } - @Override - public void onReactContextInitialized(final ReactContext context) { + @Override + public void onReactContextInitialized(final ReactContext context) { emitAppLaunched(context); - } -} + } +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/Events.kt b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/Events.kt new file mode 100644 index 00000000000..a92dbfac873 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/Events.kt @@ -0,0 +1,34 @@ +package com.reactnativenavigation.react.modal + +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +open class RequestCloseModalEvent(viewTag: Int) : Event(viewTag) { + + companion object{ + const val EVENT_NAME = "topRequestClose" + } + + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter) { + rctEventEmitter.receiveEvent(viewTag, eventName, null) + } +} + +open class ShowModalEvent(viewTag: Int) : Event(viewTag) { + + companion object{ + const val EVENT_NAME = "topShow" + } + + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter) { + rctEventEmitter.receiveEvent(viewTag, eventName, null) + } +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalContentLayout.kt b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalContentLayout.kt new file mode 100644 index 00000000000..4923f371817 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalContentLayout.kt @@ -0,0 +1,81 @@ +package com.reactnativenavigation.react.modal + +import android.content.Context +import android.view.MotionEvent +import android.view.View +import com.facebook.react.bridge.* +import com.facebook.react.uimanager.* +import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.views.view.ReactViewGroup + + +class ModalContentLayout(context: Context?) : ReactViewGroup(context), RootView{ + private var hasAdjustedSize = false + private var viewWidth = 0 + private var viewHeight = 0 + private val mJSTouchDispatcher = JSTouchDispatcher(this) + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + viewWidth = w + viewHeight = h + this.updateFirstChildView() + } + private fun updateFirstChildView() { + if (this.childCount > 0) { + hasAdjustedSize = false + val viewTag = getChildAt(0).id + val reactContext: ReactContext = this.getReactContext() + reactContext.runOnNativeModulesQueueThread(object : GuardedRunnable(reactContext) { + override fun runGuarded() { + val uiManager = this@ModalContentLayout.getReactContext().getNativeModule( + UIManagerModule::class.java + ) as UIManagerModule + uiManager.updateNodeSize( + viewTag, + this@ModalContentLayout.viewWidth, + this@ModalContentLayout.viewHeight + ) + } + }) + } else { + hasAdjustedSize = true + } + } + + override fun addView(child: View?, index: Int, params: LayoutParams?) { + super.addView(child, index, params) + if (hasAdjustedSize) { + updateFirstChildView() + } + } + override fun onChildStartedNativeGesture(androidEvent: MotionEvent?) { + mJSTouchDispatcher.onChildStartedNativeGesture(androidEvent, this.getEventDispatcher()) + } + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} + private fun getEventDispatcher(): EventDispatcher? { + val reactContext: ReactContext = this.getReactContext() + return reactContext.getNativeModule(UIManagerModule::class.java)!!.eventDispatcher + } + + + override fun handleException(t: Throwable?) { + getReactContext().handleException(RuntimeException(t)) + } + + private fun getReactContext(): ReactContext { + return this.context as ReactContext + } + + override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { + mJSTouchDispatcher.handleTouchEvent(event, getEventDispatcher()) + return super.onInterceptTouchEvent(event) + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + mJSTouchDispatcher.handleTouchEvent(event, getEventDispatcher()) + super.onTouchEvent(event) + return true + } + +} \ 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 new file mode 100644 index 00000000000..07e0690127b --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalFrameLayout.kt @@ -0,0 +1,19 @@ +package com.reactnativenavigation.react.modal + +import android.widget.FrameLayout +import com.facebook.react.bridge.ReactContext +import com.reactnativenavigation.utils.StatusBarUtils + +class ModalFrameLayout(context: ReactContext) : FrameLayout(context) { + val modalContentLayout = ModalContentLayout(context) + + init { + addView(modalContentLayout, MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT) + .apply { + val translucent = context.currentActivity?.window?.let { + StatusBarUtils.isTranslucent(context.currentActivity?.window) + } ?: false + topMargin = if (translucent) 0 else StatusBarUtils.getStatusBarHeight(context) + }) + } +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalHostLayout.kt b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalHostLayout.kt new file mode 100644 index 00000000000..121aa6a09f7 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalHostLayout.kt @@ -0,0 +1,78 @@ +package com.reactnativenavigation.react.modal + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.view.View +import android.view.ViewGroup +import android.view.ViewStructure +import android.view.accessibility.AccessibilityEvent +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativenavigation.options.Options +import com.reactnativenavigation.options.params.Bool +import com.reactnativenavigation.utils.CompatUtils +import com.reactnativenavigation.viewcontrollers.viewcontroller.YellowBoxDelegate +import com.reactnativenavigation.viewcontrollers.viewcontroller.overlay.ViewControllerOverlay +import java.util.* + +@SuppressLint("ViewConstructor") +open class ModalHostLayout(reactContext: ThemedReactContext) : ViewGroup(reactContext), LifecycleEventListener { + val viewController = ModalLayoutController( + reactContext, + reactContext.currentActivity, CompatUtils.generateViewId().toString(), + YellowBoxDelegate(reactContext), Options().apply { + hardwareBack.dismissModalOnPress = Bool(false) + }, ViewControllerOverlay(reactContext), + getHostId = { this.id } + ) + private val mHostView = viewController.view.modalContentLayout + + init { + (context as ReactContext).addLifecycleEventListener(this) + } + + @TargetApi(23) + override fun dispatchProvideStructure(structure: ViewStructure?) { + mHostView.dispatchProvideStructure(structure) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {} + + override fun addView(child: View?, index: Int) { + UiThreadUtil.assertOnUiThread() + mHostView.addView(child, index) + } + + override fun getChildCount(): Int { + return mHostView.childCount + } + + override fun getChildAt(index: Int): View? { + return mHostView.getChildAt(index) + } + + override fun removeView(child: View?) { + UiThreadUtil.assertOnUiThread() + mHostView.removeView(child) + } + + override fun removeViewAt(index: Int) { + UiThreadUtil.assertOnUiThread() + val child = getChildAt(index) + mHostView.removeView(child) + } + + override fun addChildrenForAccessibility(outChildren: ArrayList?) {} + + override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean { return false } + + open fun onDropInstance() { (this.context as ReactContext).removeLifecycleEventListener(this) } + + override fun onHostResume() {} + + override fun onHostPause() {} + + override fun onHostDestroy() { onDropInstance() } +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalLayoutController.kt b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalLayoutController.kt new file mode 100644 index 00000000000..d8fd6a15b8a --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalLayoutController.kt @@ -0,0 +1,51 @@ +package com.reactnativenavigation.react.modal + +import android.app.Activity +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.UIManagerModule +import com.reactnativenavigation.options.Options +import com.reactnativenavigation.react.Constants +import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController +import com.reactnativenavigation.viewcontrollers.viewcontroller.YellowBoxDelegate +import com.reactnativenavigation.viewcontrollers.viewcontroller.overlay.ViewControllerOverlay + +class ModalLayoutController( + val reactContext: ReactContext, + activity: Activity?, + id: String?, + yellowBoxDelegate: YellowBoxDelegate?, + initialOptions: Options?, + overlay: ViewControllerOverlay?, + val getHostId: () -> Int +) : ViewController(activity, id, yellowBoxDelegate, initialOptions, overlay) { + override fun isViewShown(): Boolean { + return !isDestroyed && view != null && view!!.isShown + } + + override fun isRendered(): Boolean { + return isViewCreated + } + + override fun getCurrentComponentName(): String = "ModalLayoutController" + + + override fun createView(): ModalFrameLayout { + return ModalFrameLayout(reactContext) + } + + override fun sendOnNavigationButtonPressed(buttonId: String?) { + if (buttonId == Constants.HARDWARE_BACK_BUTTON_ID) { + val dispatcher = reactContext.getNativeModule( + UIManagerModule::class.java + ).eventDispatcher + dispatcher.dispatchEvent(RequestCloseModalEvent(getHostId())) + } + } + + fun sendShowEvent() { + val dispatcher = reactContext.getNativeModule( + UIManagerModule::class.java + ).eventDispatcher + dispatcher.dispatchEvent(ShowModalEvent(getHostId())) + } +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalViewManager.kt b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalViewManager.kt new file mode 100644 index 00000000000..11fb497c487 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/modal/ModalViewManager.kt @@ -0,0 +1,131 @@ +package com.reactnativenavigation.react.modal + +import android.content.Context +import android.graphics.Point +import android.view.WindowManager +import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.common.MapBuilder +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.LayoutShadowNode +import com.facebook.react.uimanager.ReactShadowNodeImpl +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.annotations.ReactProp +import com.reactnativenavigation.options.ModalPresentationStyle +import com.reactnativenavigation.options.Options +import com.reactnativenavigation.options.params.Bool +import com.reactnativenavigation.options.parseTransitionAnimationOptions +import com.reactnativenavigation.options.parsers.JSONParser +import com.reactnativenavigation.react.CommandListener +import com.reactnativenavigation.react.CommandListenerAdapter +import com.reactnativenavigation.utils.StatusBarUtils +import com.reactnativenavigation.viewcontrollers.navigator.Navigator + +private const val MODAL_MANAGER_NAME = "RNNModalViewManager" + +@ReactModule(name = MODAL_MANAGER_NAME) +class ModalViewManager(private val navigator: Navigator) : ViewGroupManager() { + private val jsonParser = JSONParser() + override fun getName(): String = MODAL_MANAGER_NAME + + override fun createViewInstance(reactContext: ThemedReactContext): ModalHostLayout { + return ModalHostLayout(reactContext) + } + + override fun createShadowNodeInstance(): LayoutShadowNode { + return ModalHostShadowNode() + } + + override fun getShadowNodeClass(): Class { + return ModalHostShadowNode::class.java + } + + override fun onDropViewInstance(modal: ModalHostLayout) { + super.onDropViewInstance(modal) + navigator.dismissModal(modal.viewController.id, CommandListenerAdapter()) + modal.onDropInstance() + } + + override fun onAfterUpdateTransaction(modal: ModalHostLayout) { + super.onAfterUpdateTransaction(modal) + navigator.showModal(modal.viewController, CommandListenerAdapter(object : CommandListener { + override fun onSuccess(childId: String?) { + modal.viewController.sendShowEvent() + } + + override fun onError(message: String?) { + } + + })) + } + + override fun getExportedCustomDirectEventTypeConstants(): Map? { + return MapBuilder.builder() + .put(RequestCloseModalEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRequestClose")) + .put(ShowModalEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShow")) + .build() + } + + @ReactProp(name = "animation") + fun setAnimation(modal: ModalHostLayout, animation: ReadableMap) { + modal.viewController.mergeOptions(Options().apply { + val animationJson = jsonParser.parse(animation) + val showModal = parseTransitionAnimationOptions(animationJson.optJSONObject("showModal")) + val dismissModal = parseTransitionAnimationOptions(animationJson.optJSONObject("dismissModal")) + this.animations.showModal = showModal + this.animations.dismissModal = dismissModal + }) + } + @ReactProp(name = "blurOnUnmount") + fun setBlurOnUnmount(modal: ModalHostLayout, blurOnUnmount: Boolean) { + modal.viewController.mergeOptions(Options().apply { + this.modal.blurOnUnmount = Bool(blurOnUnmount) + }) + } + @ReactProp(name = "transparent") + fun setTransparent(modal: ModalHostLayout, transparent: Boolean) { + modal.viewController.mergeOptions(Options().apply { + this.modal.presentationStyle = if(transparent) ModalPresentationStyle.OverCurrentContext else ModalPresentationStyle.None + }) + } +} + +private fun getModalHostSize(context: Context): Point { + val MIN_POINT = Point() + val MAX_POINT = Point() + val SIZE_POINT = Point() + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val display = Assertions.assertNotNull(wm).defaultDisplay + // getCurrentSizeRange will return the min and max width and height that the window can be + display.getCurrentSizeRange(MIN_POINT, MAX_POINT) + // getSize will return the dimensions of the screen in its current orientation + display.getSize(SIZE_POINT) + val attrs = intArrayOf(android.R.attr.windowFullscreen) + val theme = context.theme + val ta = theme.obtainStyledAttributes(attrs) + val windowFullscreen = ta.getBoolean(0, false) + + // We need to add the status bar height to the height if we have a fullscreen window, + // because Display.getCurrentSizeRange doesn't include it. + var statusBarHeight = 0 + if (windowFullscreen) { + statusBarHeight = StatusBarUtils.getStatusBarHeight(context) + } + return if (SIZE_POINT.x < SIZE_POINT.y) { + // If we are vertical the width value comes from min width and height comes from max height + Point(MIN_POINT.x, MAX_POINT.y + statusBarHeight) + } else { + // If we are horizontal the width value comes from max width and height comes from min height + Point(MAX_POINT.x, MIN_POINT.y + statusBarHeight) + } +} + +private class ModalHostShadowNode : LayoutShadowNode() { + override fun addChildAt(child: ReactShadowNodeImpl, i: Int) { + super.addChildAt(child, i) + val modalSize = getModalHostSize(themedContext) + child.setStyleWidth(modalSize.x.toFloat()) + child.setStyleHeight(modalSize.y.toFloat()) + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentPresenterBase.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentPresenterBase.java index 0fd354bdae0..47828725f28 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentPresenterBase.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentPresenterBase.java @@ -2,11 +2,13 @@ import android.view.View; import android.view.ViewGroup.MarginLayoutParams; +import android.view.WindowManager; import androidx.annotation.NonNull; public class ComponentPresenterBase { public void applyTopInsets(@NonNull View view, int topInsets) { + if(!(view.getLayoutParams() instanceof MarginLayoutParams)) return; MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); if (lp != null && lp.topMargin != topInsets) { lp.topMargin = topInsets; @@ -15,6 +17,8 @@ public void applyTopInsets(@NonNull View view, int topInsets) { } public void applyBottomInset(@NonNull View view, int bottomInset) { + if(!(view.getLayoutParams() instanceof MarginLayoutParams)) return; + MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); if (lp != null && lp.bottomMargin!= bottomInset) { lp.bottomMargin = bottomInset; diff --git a/lib/src/adapters/TouchablePreview.tsx b/lib/src/adapters/TouchablePreview.tsx index 67586395516..fb2e743501d 100644 --- a/lib/src/adapters/TouchablePreview.tsx +++ b/lib/src/adapters/TouchablePreview.tsx @@ -101,7 +101,7 @@ export class TouchablePreview extends React.PureComponent { this.props.onPeekIn(); } } - + //@ts-ignore this.timeout = setTimeout(this.onTouchEnd, PREVIEW_TIMEOUT); }; diff --git a/lib/src/commands/Commands.test.ts b/lib/src/commands/Commands.test.ts index c0465eb0774..4b75df04a47 100644 --- a/lib/src/commands/Commands.test.ts +++ b/lib/src/commands/Commands.test.ts @@ -243,8 +243,8 @@ describe('Commands', () => { ); }); - test('update props with callback', done => { - function callback () { + test('update props with callback', (done) => { + function callback() { try { expect(true).toBe(true); done(); diff --git a/lib/src/components/ComponentWrapper.test.tsx b/lib/src/components/ComponentWrapper.test.tsx index a5e1d709160..417c3c82459 100644 --- a/lib/src/components/ComponentWrapper.test.tsx +++ b/lib/src/components/ComponentWrapper.test.tsx @@ -121,8 +121,8 @@ describe('ComponentWrapper', () => { }); }); - test('update props with callback', done => { - function callback () { + test('update props with callback', (done) => { + function callback() { try { expect(true).toBe(true); done(); diff --git a/lib/src/components/ComponentWrapper.tsx b/lib/src/components/ComponentWrapper.tsx index f6444a33dfd..54283f20173 100644 --- a/lib/src/components/ComponentWrapper.tsx +++ b/lib/src/components/ComponentWrapper.tsx @@ -57,12 +57,15 @@ export class ComponentWrapper { } public setProps(newProps: any, callback?: () => void) { - this.setState((prevState) => ({ - allProps: { - ...prevState.allProps, - ...newProps, - }, - }), callback); + this.setState( + (prevState) => ({ + allProps: { + ...prevState.allProps, + ...newProps, + }, + }), + callback + ); } componentDidMount() { diff --git a/lib/src/components/Modal.tsx b/lib/src/components/Modal.tsx new file mode 100644 index 00000000000..23ecc5e3422 --- /dev/null +++ b/lib/src/components/Modal.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { requireNativeComponent, ViewProps, StyleSheet, Dimensions } from 'react-native'; +import { AnimationOptions, ViewAnimationOptions } from 'react-native-navigation/interfaces/Options'; +import { View } from 'react-native-ui-lib'; +export interface RNNModalProps extends ViewProps { + visible: boolean; + transparent: boolean; + blurOnUnmount: boolean; + animationType: 'none' | 'fade' | 'slide'; + onShow?: () => any; + onRequestClose: () => any; +} +interface AnimatedModalProps extends RNNModalProps { + animation?: AnimationOptions; +} +const RNNModalViewManager = requireNativeComponent('RNNModalViewManager'); + +export class Modal extends React.Component { + static defaultProps = { + transparent: false, + blurOnUnmount: false, + animationType: 'slide', + }; + constructor(props: RNNModalProps) { + super(props); + } + render() { + const processed = this.proccessProps(); + if (this.props.visible) { + return ( + + + {this.props.children} + + + ); + } else { + return null; + } + } + + private proccessProps() { + const processed: AnimatedModalProps = { ...this.props, style: styles.modal }; + if (this.props.animationType === 'none') { + processed.animation = { + showModal: { enabled: false }, + dismissModal: { enabled: false }, + }; + } else { + const isSlide = this.props.animationType === 'slide'; + processed.animation = { + showModal: { + enter: isSlide ? showModalSlideEnterAnimations : showModalFadeEnterAnimations, + }, + dismissModal: { + exit: isSlide ? dismissModalSlideExitAnimations : dismissModalFadeExitAnimations, + }, + }; + } + return processed; + } +} + +const height = Math.round(Dimensions.get('window').height); +const SCREEN_ANIMATION_DURATION = 500; +const showModalSlideEnterAnimations: ViewAnimationOptions = { + translationY: { + from: height, + to: 0, + duration: SCREEN_ANIMATION_DURATION, + interpolation: { type: 'decelerate' }, + }, +}; + +const dismissModalSlideExitAnimations: ViewAnimationOptions = { + translationY: { + from: 0, + to: height, + duration: SCREEN_ANIMATION_DURATION, + interpolation: { type: 'decelerate' }, + }, +}; +const showModalFadeEnterAnimations: ViewAnimationOptions = { + alpha: { + from: 0, + to: 1, + duration: SCREEN_ANIMATION_DURATION, + interpolation: { type: 'decelerate' }, + }, +}; + +const dismissModalFadeExitAnimations: ViewAnimationOptions = { + alpha: { + from: 1, + to: 0, + duration: SCREEN_ANIMATION_DURATION, + interpolation: { type: 'decelerate' }, + }, +}; + +const styles = StyleSheet.create({ + modal: { + position: 'absolute', + }, + container: { + top: 0, + flex: 1, + }, +}); diff --git a/lib/src/components/Store.test.ts b/lib/src/components/Store.test.ts index 454c18c0d70..f458e3e73a3 100644 --- a/lib/src/components/Store.test.ts +++ b/lib/src/components/Store.test.ts @@ -23,8 +23,8 @@ describe('Store', () => { expect(uut.getPropsForId('component1')).toEqual({}); }); - test('update props with callback', done => { - function callback () { + test('update props with callback', (done) => { + function callback() { try { expect(true).toBe(true); done(); diff --git a/lib/src/index.ts b/lib/src/index.ts index 8a44e8aa9e6..230459f720c 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -1,7 +1,7 @@ import { NavigationDelegate } from './NavigationDelegate'; const navigationDelegate = new NavigationDelegate(); - export const Navigation = navigationDelegate; +export * from './components/Modal'; export * from './events/EventsRegistry'; export * from './adapters/Constants'; export * from './interfaces/ComponentEvents'; diff --git a/playground/src/screens/ModalScreen.tsx b/playground/src/screens/ModalScreen.tsx index bba3c40595e..3062fb6b2ca 100644 --- a/playground/src/screens/ModalScreen.tsx +++ b/playground/src/screens/ModalScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NavigationComponent } from 'react-native-navigation'; +import { NavigationComponent, Modal as RNNModal } from 'react-native-navigation'; import last from 'lodash/last'; import concat from 'lodash/concat'; import forEach from 'lodash/forEach'; @@ -12,14 +12,17 @@ import { stack } from '../commons/Layouts'; import Screens from './Screens'; import flags from '../flags'; import testIDs from '../testIDs'; -import { Dimensions, Modal } from 'react-native'; +import { Dimensions, Modal, Image, Platform, StyleSheet } from 'react-native'; +import { View } from 'react-native-ui-lib'; + const height = Math.round(Dimensions.get('window').height); const MODAL_ANIMATION_DURATION = 350; - const { PUSH_BTN, MODAL_SCREEN_HEADER, MODAL_BTN, + SHOW_MODAL_FROM_DECLARED_BUTTON, + OVERLAY_BTN, MODAL_DISABLED_BACK_BTN, DISMISS_MODAL_BTN, DISMISS_UNKNOWN_MODAL_BTN, @@ -30,8 +33,7 @@ const { DISMISS_ALL_MODALS_BTN, DISMISS_FIRST_MODAL_BTN, SET_ROOT, - TOGGLE_REACT_NATIVE_MODAL, - SHOW_MODAL_AND_DISMISS_REACT_NATIVE_MODAL, + TOGGLE_REACT_DECLARED_MODAL, } = testIDs; interface Props { @@ -41,7 +43,7 @@ interface Props { interface State { swipeableToDismiss: boolean; - reactNativeModalVisible: boolean; + modalVisible: boolean; } export default class ModalScreen extends NavigationComponent { @@ -60,7 +62,7 @@ export default class ModalScreen extends NavigationComponent { super(props); this.state = { swipeableToDismiss: false, - reactNativeModalVisible: false, + modalVisible: false, }; } @@ -125,34 +127,73 @@ export default class ModalScreen extends NavigationComponent {