Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support react-native modals #7156

Merged
merged 10 commits into from
Jun 17, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions e2e/Modals.test.js
Original file line number Diff line number Diff line change
@@ -158,4 +158,11 @@ describe('modal', () => {
'dismissModal promise resolved with: UniqueStackId'
);
});

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();
await elementById(TestIDs.DISMISS_MODAL_BTN).tap();
await expect(elementById(TestIDs.MODAL_SCREEN_HEADER)).toBeVisible();
});
});
8 changes: 5 additions & 3 deletions lib/ios/RNNBridgeManager.mm
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#import "RNNBridgeManager.h"

#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>

#import "RNNBridgeModule.h"
#import "RNNComponentViewCreator.h"
#import "RNNEventEmitter.h"
#import "RNNLayoutManager.h"
#import "RNNReactComponentRegistry.h"
#import "RNNReactRootViewCreator.h"
#import "RNNSplashScreen.h"
#import <React/RCTBridge.h>
#import <React/RCTModalHostViewManager.h>
#import <React/RCTUIManager.h>

@interface RNNBridgeManager ()

@@ -105,6 +105,8 @@ - (void)onJavaScriptWillLoad {

- (void)onJavaScriptLoaded {
[_commandsHandler setReadyToReceiveCommands:true];
[_modalManager
connectModalHostViewManager:[self.bridge moduleForClass:RCTModalHostViewManager.class]];
[[_bridge moduleForClass:[RNNEventEmitter class]] sendOnAppLaunched];
}

22 changes: 16 additions & 6 deletions lib/ios/RNNCommandsHandler.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "RNNCommandsHandler.h"
#import "RNNAssert.h"
#import "RNNComponentViewController.h"
#import "RNNConvert.h"
#import "RNNDefaultOptionsHelper.h"
#import "RNNErrorHandler.h"
#import "React/RCTI18nUtil.h"
@@ -368,6 +369,12 @@ - (void)showModal:(NSDictionary *)layout
__weak UIViewController *weakNewVC = newVc;
newVc.waitForRender =
[newVc.resolveOptionsWithDefault.animations.showModal shouldWaitForRender];
newVc.modalPresentationStyle =
[RNNConvert UIModalPresentationStyle:[newVc.resolveOptionsWithDefault.modalPresentationStyle
withDefault:@"default"]];
newVc.modalTransitionStyle =
[RNNConvert UIModalTransitionStyle:[newVc.resolveOptionsWithDefault.modalTransitionStyle
withDefault:@"coverVertical"]];
[newVc setReactViewReadyCallback:^{
[self->_modalManager
showModal:weakNewVC
@@ -403,12 +410,15 @@ - (void)dismissModal:(NSString *)componentId
RNNNavigationOptions *options = [[RNNNavigationOptions alloc] initWithDict:mergeOptions];
[modalToDismiss.presentedComponentViewController mergeOptions:options];

[_modalManager dismissModal:modalToDismiss
completion:^{
[self->_eventEmitter sendOnNavigationCommandCompletion:dismissModal
commandId:commandId];
completion(modalToDismiss.topMostViewController.layoutInfo.componentId);
}];
[_modalManager
dismissModal:modalToDismiss
animated:[modalToDismiss.resolveOptionsWithDefault.animations.dismissModal.exit.enable
withDefault:YES]
completion:^{
[self->_eventEmitter sendOnNavigationCommandCompletion:dismissModal
commandId:commandId];
completion(modalToDismiss.topMostViewController.layoutInfo.componentId);
}];
}

- (void)dismissAllModals:(NSDictionary *)mergeOptions
4 changes: 2 additions & 2 deletions lib/ios/RNNEnterExitAnimation.m
Original file line number Diff line number Diff line change
@@ -12,9 +12,9 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
}

- (void)mergeOptions:(RNNEnterExitAnimation *)options {
if (options.enter.hasAnimation)
if (options.enter.hasValue)
self.enter = options.enter;
if (options.exit.hasAnimation)
if (options.exit.hasValue)
self.exit = options.exit;
}

6 changes: 4 additions & 2 deletions lib/ios/RNNEventEmitter.m
Original file line number Diff line number Diff line change
@@ -124,8 +124,10 @@ - (void)sendOnPreviewCompleted:(NSString *)componentId

- (void)sendModalsDismissedEvent:(NSString *)componentId
numberOfModalsDismissed:(NSNumber *)modalsDismissed {
[self send:ModalDismissed
body:@{@"componentId" : componentId, @"modalsDismissed" : modalsDismissed}];
if (componentId) {
[self send:ModalDismissed
body:@{@"componentId" : componentId, @"modalsDismissed" : modalsDismissed}];
}
}

- (void)sendModalAttemptedToDismissEvent:(NSString *)componentId {
3 changes: 3 additions & 0 deletions lib/ios/RNNModalManager.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "RNNModalManagerEventHandler.h"
#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>
#import <React/RCTModalHostViewManager.h>
#import <UIKit/UIKit.h>

typedef void (^RNNTransitionCompletionBlock)(void);
@@ -12,11 +13,13 @@ typedef void (^RNNTransitionRejectionBlock)(NSString *_Nonnull code, NSString *_

- (instancetype _Nonnull)initWithBridge:(RCTBridge *_Nonnull)bridge
eventHandler:(RNNModalManagerEventHandler *_Nonnull)eventHandler;
- (void)connectModalHostViewManager:(RCTModalHostViewManager *_Nonnull)modalHostViewManager;

- (void)showModal:(UIViewController *_Nonnull)viewController
animated:(BOOL)animated
completion:(RNNTransitionWithComponentIdCompletionBlock _Nullable)completion;
- (void)dismissModal:(UIViewController *_Nullable)viewController
animated:(BOOL)animated
completion:(RNNTransitionCompletionBlock _Nullable)completion;
- (void)dismissAllModalsAnimated:(BOOL)animated completion:(void (^__nullable)(void))completion;
- (void)dismissAllModalsSynchronosly;
51 changes: 33 additions & 18 deletions lib/ios/RNNModalManager.m
Original file line number Diff line number Diff line change
@@ -33,6 +33,30 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
return self;
}

- (void)connectModalHostViewManager:(RCTModalHostViewManager *)modalHostViewManager {
modalHostViewManager.presentationBlock =
^(UIViewController *reactViewController, UIViewController *viewController, BOOL animated,
dispatch_block_t completionBlock) {
[self showModal:viewController
animated:animated
completion:^(NSString *_Nonnull componentId) {
if (completionBlock)
completionBlock();
}];
};

modalHostViewManager.dismissalBlock =
^(UIViewController *reactViewController, UIViewController *viewController, BOOL animated,
dispatch_block_t completionBlock) {
[self dismissModal:viewController
animated:animated
completion:^{
if (completionBlock)
completionBlock();
}];
};
}

- (void)showModal:(UIViewController<RNNLayoutProtocol> *)viewController
animated:(BOOL)animated
completion:(RNNTransitionWithComponentIdCompletionBlock)completion {
@@ -44,13 +68,6 @@ - (void)showModal:(UIViewController<RNNLayoutProtocol> *)viewController

UIViewController *topVC = [self topPresentedVC];

viewController.modalPresentationStyle = [RNNConvert
UIModalPresentationStyle:[viewController.resolveOptionsWithDefault.modalPresentationStyle
withDefault:@"default"]];
viewController.modalTransitionStyle = [RNNConvert
UIModalTransitionStyle:[viewController.resolveOptionsWithDefault.modalTransitionStyle
withDefault:@"coverVertical"]];

if (viewController.presentationController) {
viewController.presentationController.delegate = self;
}
@@ -80,10 +97,11 @@ - (void)showModal:(UIViewController<RNNLayoutProtocol> *)viewController
}

- (void)dismissModal:(UIViewController *)viewController
animated:(BOOL)animated
completion:(RNNTransitionCompletionBlock)completion {
if (viewController) {
[_pendingModalIdsToDismiss addObject:viewController];
[self removePendingNextModalIfOnTop:completion];
[self removePendingNextModalIfOnTop:completion animated:animated];
}
}

@@ -128,7 +146,8 @@ - (void)dismissAllModalsSynchronosly {

#pragma mark - private

- (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion {
- (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion
animated:(BOOL)animated {
UIViewController<RNNLayoutProtocol> *modalToDismiss = [_pendingModalIdsToDismiss lastObject];
RNNNavigationOptions *optionsWithDefault = modalToDismiss.resolveOptionsWithDefault;

@@ -152,12 +171,11 @@ - (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion {
_dismissModalTransitionDelegate;
}

if (modalToDismiss == topPresentedVC ||
[[topPresentedVC childViewControllers] containsObject:modalToDismiss]) {
if ((modalToDismiss == topPresentedVC ||
[[topPresentedVC childViewControllers] containsObject:modalToDismiss])) {
[self dismissSearchController:modalToDismiss];
[modalToDismiss
dismissViewControllerAnimated:[optionsWithDefault.animations.dismissModal.exit.enable
withDefault:YES]
dismissViewControllerAnimated:animated
completion:^{
[self->_pendingModalIdsToDismiss removeObject:modalToDismiss];
if (modalToDismiss.view) {
@@ -168,18 +186,15 @@ - (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion {
completion();
}

[self removePendingNextModalIfOnTop:nil];
[self removePendingNextModalIfOnTop:nil animated:NO];
}];
} else {
[modalToDismiss.view removeFromSuperview];
modalToDismiss.view = nil;
modalToDismiss.getCurrentChild.resolveOptions.animations.dismissModal.exit.enable =
[[Bool alloc] initWithBOOL:NO];
[self dismissedModal:modalToDismiss];

if (completion) {
if (completion)
completion();
}
}
}

1 change: 1 addition & 0 deletions lib/ios/TransitionOptions.h
Original file line number Diff line number Diff line change
@@ -17,5 +17,6 @@

- (NSTimeInterval)maxDuration;
- (BOOL)hasAnimation;
- (BOOL)hasValue;

@end
6 changes: 6 additions & 0 deletions lib/ios/TransitionOptions.m
Original file line number Diff line number Diff line change
@@ -35,6 +35,12 @@ - (void)mergeOptions:(TransitionOptions *)options {
}

- (BOOL)hasAnimation {
return self.x.hasAnimation || self.y.hasAnimation || self.alpha.hasAnimation ||
self.translationX.hasAnimation || self.translationY.hasAnimation ||
self.rotationX.hasAnimation || self.rotationY.hasAnimation;
}

- (BOOL)hasValue {
return self.x.hasAnimation || self.y.hasAnimation || self.alpha.hasAnimation ||
self.translationX.hasAnimation || self.translationY.hasAnimation ||
self.rotationX.hasAnimation || self.rotationY.hasAnimation ||
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -177,13 +177,13 @@
"binaryPath": "playground/android/app/build/outputs/apk/debug/app-debug.apk",
"build": "cd playground/android && ./gradlew app:assembleDebug app:assembleAndroidTest -DtestBuildType=debug",
"type": "android.emulator",
"name": "Pixel_API_28"
"name": "Pixel_3A_API_29"
},
"android.emu.release": {
"binaryPath": "playground/android/app/build/outputs/apk/release/app-release.apk",
"build": "cd playground/android && ./gradlew app:assembleRelease app:assembleAndroidTest -DtestBuildType=release",
"type": "android.emulator",
"name": "Pixel_API_28"
"name": "Pixel_3A_API_29"
}
}
}
23 changes: 21 additions & 2 deletions playground/ios/NavigationTests/RNNCommandsHandlerTest.m
Original file line number Diff line number Diff line change
@@ -749,7 +749,7 @@ - (void)testDismissModal_shouldResolveTopMostComponentId {
eventEmitter:nil
childViewControllers:@[ child ]];

OCMStub([self.modalManager dismissModal:OCMArg.any completion:OCMArg.invokeBlock]);
OCMStub([self.modalManager dismissModal:OCMArg.any animated:NO completion:OCMArg.invokeBlock]);
OCMStub(child.isModal).andReturn(YES);
OCMStub([self.layoutManager findComponentForId:@"child"]).andReturn(child);

@@ -787,11 +787,12 @@ - (void)testDismissModal_shouldMergeOptions {
dismissModal:[OCMArg checkWithBlock:^BOOL(UIViewController *modalToDismiss) {
return modalToDismiss.options.animations.dismissModal.exit.enable.get == NO;
}]
animated:NO
completion:OCMArg.any];

[self.uut dismissModal:@"child"
commandId:@"commandId"
mergeOptions:@{@"animations" : @{@"dismissModal" : @{@"enabled" : @(0)}}}
mergeOptions:@{@"animations" : @{@"dismissModal" : @{@"exit" : @{@"enabled" : @(0)}}}}
completion:^(NSString *_Nonnull componentId) {
XCTAssertTrue([componentId isEqualToString:@"stack"]);
}
@@ -802,6 +803,24 @@ - (void)testDismissModal_shouldMergeOptions {
[self.modalManager verify];
}

- (void)testShowModal_withPresentationStyle {
[self.uut setReadyToReceiveCommands:true];
OCMStub([self.controllerFactory createLayout:[OCMArg any]]).andReturn(_vc1);
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalPresentationStyle = [Text withValue:@"overCurrentContext"];
[self.uut showModal:@{} commandId:@"" completion:nil];
XCTAssertEqual(_vc1.modalPresentationStyle, UIModalPresentationOverCurrentContext);
}

- (void)testApplyOptionsOnInit_shouldShowModalWithTransitionStyle {
[self.uut setReadyToReceiveCommands:true];
OCMStub([self.controllerFactory createLayout:[OCMArg any]]).andReturn(_vc1);
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalTransitionStyle = [Text withValue:@"crossDissolve"];
[self.uut showModal:@{} commandId:@"" completion:nil];
XCTAssertEqual(_vc1.modalTransitionStyle, UIModalTransitionStyleCrossDissolve);
}

- (void)testPush_shouldResolvePromiseAndSendCommandCompletionWithPushedComponentId {
[self.uut setReadyToReceiveCommands:true];
NSString *expectedComponentId = @"pushedComponent";
22 changes: 4 additions & 18 deletions playground/ios/NavigationTests/RNNModalManagerTest.m
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ - (void)testDismissModal_InvokeDelegateWithCorrectParameters {
[_modalManager showModal:_vc3 animated:NO completion:nil];

[[_modalManagerEventHandler expect] dismissedModal:_vc3];
[_modalManager dismissModal:_vc3 completion:nil];
[_modalManager dismissModal:_vc3 animated:NO completion:nil];
[_modalManagerEventHandler verify];
}

@@ -99,7 +99,7 @@ - (void)testDismissPreviousModal_InvokeDelegateWithCorrectParameters {
[_modalManager showModal:_vc3 animated:NO completion:nil];

[[_modalManagerEventHandler expect] dismissedModal:_vc2];
[_modalManager dismissModal:_vc2 completion:nil];
[_modalManager dismissModal:_vc2 animated:NO completion:nil];
[_modalManagerEventHandler verify];
}

@@ -113,7 +113,7 @@ - (void)testDismissAllModals_AfterDismissingPreviousModal_InvokeDelegateWithCorr
[_modalManager showModal:_vc3 animated:NO completion:nil];

[[_modalManagerEventHandler expect] dismissedModal:_vc2];
[_modalManager dismissModal:_vc2 completion:nil];
[_modalManager dismissModal:_vc2 animated:NO completion:nil];
[_modalManagerEventHandler verify];

[[_modalManagerEventHandler expect] dismissedMultipleModals:@[ _vc1, _vc3 ]];
@@ -123,7 +123,7 @@ - (void)testDismissAllModals_AfterDismissingPreviousModal_InvokeDelegateWithCorr

- (void)testDismissModal_DismissNilModalDoesntCrash {
[[_modalManagerEventHandler reject] dismissedModal:OCMArg.any];
[_modalManager dismissModal:nil completion:nil];
[_modalManager dismissModal:nil animated:NO completion:nil];
[_modalManagerEventHandler verify];
}

@@ -170,18 +170,4 @@ - (void)testApplyOptionsOnInit_shouldShowModalWithDefaultTransitionStyle {
XCTAssertEqual(_vc1.modalTransitionStyle, UIModalTransitionStyleCoverVertical);
}

- (void)testApplyOptionsOnInit_shouldShowModalWithPresentationStyle {
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalPresentationStyle = [Text withValue:@"overCurrentContext"];
[_modalManager showModal:_vc1 animated:NO completion:nil];
XCTAssertEqual(_vc1.modalPresentationStyle, UIModalPresentationOverCurrentContext);
}

- (void)testApplyOptionsOnInit_shouldShowModalWithTransitionStyle {
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalTransitionStyle = [Text withValue:@"crossDissolve"];
[_modalManager showModal:_vc1 animated:NO completion:nil];
XCTAssertEqual(_vc1.modalTransitionStyle, UIModalTransitionStyleCrossDissolve);
}

@end
32 changes: 31 additions & 1 deletion playground/src/screens/ModalScreen.tsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { stack } from '../commons/Layouts';
import Screens from './Screens';
import flags from '../flags';
import testIDs from '../testIDs';
import { Dimensions } from 'react-native';
import { Dimensions, Modal } from 'react-native';
const height = Math.round(Dimensions.get('window').height);
const MODAL_ANIMATION_DURATION = 350;

@@ -30,6 +30,8 @@ const {
DISMISS_ALL_MODALS_BTN,
DISMISS_FIRST_MODAL_BTN,
SET_ROOT,
TOGGLE_REACT_NATIVE_MODAL,
SHOW_MODAL_AND_DISMISS_REACT_NATIVE_MODAL,
} = testIDs;

interface Props {
@@ -39,6 +41,7 @@ interface Props {

interface State {
swipeableToDismiss: boolean;
reactNativeModalVisible: boolean;
}

export default class ModalScreen extends NavigationComponent<Props, State> {
@@ -57,6 +60,7 @@ export default class ModalScreen extends NavigationComponent<Props, State> {
super(props);
this.state = {
swipeableToDismiss: false,
reactNativeModalVisible: false,
};
}

@@ -124,10 +128,36 @@ export default class ModalScreen extends NavigationComponent<Props, State> {
label={`Toggle to swipeToDismiss: ${this.state.swipeableToDismiss}`}
onPress={this.toggleSwipeToDismiss}
/>
<Button
label="Toggle react-native modal"
testID={TOGGLE_REACT_NATIVE_MODAL}
onPress={this.toggleModal}
/>

<Modal visible={this.state.reactNativeModalVisible} animationType={'slide'}>
<Root>
<Button label="Toggle react-native modal" onPress={this.toggleModal} />
<Button
label="Present another modal and dismiss current modal"
testID={SHOW_MODAL_AND_DISMISS_REACT_NATIVE_MODAL}
onPress={async () => {
await Navigation.showModal({
component: {
name: Screens.Modal,
},
});
this.toggleModal();
}}
/>
</Root>
</Modal>
</Root>
);
}

toggleModal = () =>
this.setState({ reactNativeModalVisible: !this.state.reactNativeModalVisible });

showModalWithTransition = () => {
Navigation.showModal({
component: {
2 changes: 2 additions & 0 deletions playground/src/testIDs.ts
Original file line number Diff line number Diff line change
@@ -189,6 +189,8 @@ const testIDs = {
SIDE_MENU_LEFT_DRAWER_HEIGHT_TEXT: 'SIDE_MENU_LEFT_DRAWER_HEIGHT_TEXT',
SIDE_MENU_LEFT_DRAWER_WIDTH_TEXT: 'SIDE_MENU_LEFT_DRAWER_WIDTH_TEXT',
SIDE_MENU_RIGHT_DRAWER_WIDTH_TEXT: 'SIDE_MENU_RIGHT_DRAWER_WIDTH_TEXT',
TOGGLE_REACT_NATIVE_MODAL: 'TOGGLE_REACT_NATIVE_MODAL',
SHOW_MODAL_AND_DISMISS_REACT_NATIVE_MODAL: 'SHOW_MODAL_AND_DISMISS_REACT_NATIVE_MODAL',

//External Component Buttons
EXTERNAL_DISMISS_MODAL_BTN: 'EXTERNAL_DISMISS_MODAL_BTN',