feat(modals): Enable modal dialog chaining in IOS (#6637)

* feat(modals): fire close callback after close in IOS

* chore(tests): Fix some test depending on the order of events
This commit is contained in:
Alexander Vakrilov
2018-11-30 12:05:33 +02:00
committed by Dimitar Topuzov
parent bc68773bd2
commit 64bccb9bbc
9 changed files with 92 additions and 52 deletions

View File

@ -1,32 +1,29 @@
import * as pages from "tns-core-modules/ui/page"; import { Page, ShownModallyData } from "tns-core-modules/ui/page";
import * as textField from "tns-core-modules/ui/text-field"; import { EventData, fromObject } from "tns-core-modules/data/observable";
import * as observable from "tns-core-modules/data/observable";
var context: any; export function onShowingModally(args: ShownModallyData) {
var closeCallback: Function; console.log("login-page.onShowingModally, context: " + args.context);
const page = <Page>args.object;
var page: pages.Page; page.bindingContext = fromObject({
var usernameTextField: textField.TextField; username: "username",
var passwordTextField: textField.TextField; password: "password",
context: args.context,
export function onShownModally(args: pages.ShownModallyData) { onLoginButtonTap: function() {
console.log("login-page.onShownModally, context: " + args.context); console.log("login-page.onLoginButtonTap");
context = args.context; args.closeCallback(this.username, this.password);
closeCallback = args.closeCallback; }
})
} }
export function onLoaded(args: observable.EventData) { export function onShownModally(args: ShownModallyData) {
console.log("login-page.onShownModally, context: " + args.context);
}
export function onLoaded(args: EventData) {
console.log("login-page.onLoaded"); console.log("login-page.onLoaded");
page = <pages.Page>args.object;
usernameTextField = page.getViewById<textField.TextField>("username");
passwordTextField = page.getViewById<textField.TextField>("password");
} }
export function onUnloaded() { export function onUnloaded() {
console.log("login-page.onUnloaded"); console.log("login-page.onUnloaded");
} }
export function onLoginButtonTap() {
console.log("login-page.onLoginButtonTap");
closeCallback(usernameTextField.text, passwordTextField.text);
}

View File

@ -1,8 +1,11 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" shownModally="onShownModally" <Page xmlns="http://schemas.nativescript.org/tns.xsd"
showingModally="onShowingModally"
shownModally="onShownModally"
loaded="onLoaded" unloaded="onUnloaded" backgroundColor="Red"> loaded="onLoaded" unloaded="onUnloaded" backgroundColor="Red">
<StackLayout backgroundColor="PaleGreen" margin="10"> <StackLayout backgroundColor="PaleGreen" margin="10">
<TextField hint="username" id="username" text="username"/> <Label text="{{ context }}"/>
<TextField hint="password" id="password" text="password" secure="true"/> <TextField hint="username" text="{{ username }}"/>
<Button text="Login" tap="onLoginButtonTap"/> <TextField hint="password" text="{{ password }}" secure="true"/>
<Button text="Login" tap="{{ onLoginButtonTap }}"/>
</StackLayout> </StackLayout>
</Page> </Page>

View File

@ -16,9 +16,39 @@ export function onTapStretched(args) {
const label = page.getViewById<Label>("label"); const label = page.getViewById<Label>("label");
var fullscreen = false; var fullscreen = false;
var stretched = true; var stretched = true;
page.showModal("ui-tests-app/modal-view/login-page", "context", function (username: string, password: string) { page.showModal("ui-tests-app/modal-view/login-page", "context", function (username: string, password: string) {
console.log(username + "/" + password); console.log(username + "/" + password);
label.text = username + "/" + password; label.text = username + "/" + password;
}, fullscreen, false, stretched); }, fullscreen, false, stretched);
} }
function openModal(page: Page, label: Label, context: string) {
page.showModal("ui-tests-app/modal-view/login-page", context, function (username: string, password: string) {
const result = context + "/" + username + "/" + password;
console.log(result);
label.text = result;
}, false);
}
export function onTapSecondModalInCB(args) {
const page = <Page>args.object.page;
const label = page.getViewById<Label>("label");
page.showModal("ui-tests-app/modal-view/login-page", "First", function (username: string, password: string) {
const result = "First/" + username + "/" + password;
console.log(result);
label.text = result;
// Open second modal in the close callback of the first one.
openModal(page, label, "Second");
});
}
export function onTapSecondModalInTimer(args) {
const page = <Page>args.object.page;
const label = page.getViewById<Label>("label");
openModal(page, label, "First");
// Open second modal 1s after the first one.
setTimeout(() => openModal(page, label, "Second"), 1000);
}

View File

@ -3,6 +3,10 @@
<Button text="Login (pop-up)" tap="onTap" /> <Button text="Login (pop-up)" tap="onTap" />
<Button text="Login (full-screen)" tap="onTap" /> <Button text="Login (full-screen)" tap="onTap" />
<Button text="Login (pop-up-stretched)" tap="onTapStretched" /> <Button text="Login (pop-up-stretched)" tap="onTapStretched" />
<Button text="Login (second modal in cb)" tap="onTapSecondModalInCB" />
<Button text="Login (second modal in timer)" tap="onTapSecondModalInTimer" />
<Label id="label" text="Anonymous"/> <Label id="label" text="Anonymous"/>
</StackLayout> </StackLayout>
</Page> </Page>

View File

@ -738,6 +738,7 @@ export function test_WhenPageIsNavigatedToItCanShowAnotherPageAsModal() {
TKUnit.assertTrue(ctx.shownModally, "Modal-page must be shown!"); TKUnit.assertTrue(ctx.shownModally, "Modal-page must be shown!");
TKUnit.assertEqual(returnValue, "return value", "Modal-page must return value!"); TKUnit.assertEqual(returnValue, "return value", "Modal-page must return value!");
modalClosed = true; modalClosed = true;
TKUnit.assertNull(masterPage.modal, "currentPage.modal should be undefined when no modal page is shown!");
} }
let modalPage: Page; let modalPage: Page;
@ -758,7 +759,6 @@ export function test_WhenPageIsNavigatedToItCanShowAnotherPageAsModal() {
const onModalUnloaded = function (args: EventData) { const onModalUnloaded = function (args: EventData) {
modalUnloaded++; modalUnloaded++;
modalPage.off(Page.unloadedEvent, onModalUnloaded); modalPage.off(Page.unloadedEvent, onModalUnloaded);
TKUnit.assertNull(masterPage.modal, "currentPage.modal should be undefined when no modal page is shown!");
} }
const navigatedToEventHandler = function (args) { const navigatedToEventHandler = function (args) {

View File

@ -37,7 +37,6 @@ export function test_WhenShowingModalPageUnloadedIsNotFiredForTheMasterPage() {
let onModalUnloaded = function (args: EventData) { let onModalUnloaded = function (args: EventData) {
modalUnloaded++; modalUnloaded++;
modalPage.off(Page.unloadedEvent, onModalUnloaded); modalPage.off(Page.unloadedEvent, onModalUnloaded);
TKUnit.assertNull(masterPage.modal, "currentPage.modal should be undefined when no modal page is shown!");
} }
var navigatedToEventHandler = function (args) { var navigatedToEventHandler = function (args) {

View File

@ -225,10 +225,10 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
const animated = arguments[4]; const animated = arguments[4];
const stretched = arguments[5]; const stretched = arguments[5];
const view: ViewDefinition = firstAgrument instanceof ViewCommon const view = firstAgrument instanceof ViewCommon
? firstAgrument : createViewFromEntry({ moduleName: firstAgrument }); ? firstAgrument : <ViewCommon>createViewFromEntry({ moduleName: firstAgrument });
(<ViewCommon>view)._showNativeModalView(this, context, closeCallback, fullscreen, animated, stretched); view._showNativeModalView(this, context, closeCallback, fullscreen, animated, stretched);
return view; return view;
} }
} }
@ -256,27 +256,29 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this._modalParent = parent; this._modalParent = parent;
this._modalContext = context; this._modalContext = context;
const that = this; const that = this;
this._closeModalCallback = function () { this._closeModalCallback = function (...originalArgs) {
if (that._closeModalCallback) { if (that._closeModalCallback) {
const modalIndex = _rootModalViews.indexOf(that); const modalIndex = _rootModalViews.indexOf(that);
_rootModalViews.splice(modalIndex); _rootModalViews.splice(modalIndex);
that._hideNativeModalView(parent);
that._modalParent = null;
that._modalContext = null;
that._closeModalCallback = null;
that._dialogClosed();
parent._modal = null;
if (typeof closeCallback === "function") { const whenClosedCallback = () => {
closeCallback.apply(undefined, arguments); that._modalParent = null;
that._modalContext = null;
that._closeModalCallback = null;
that._dialogClosed();
parent._modal = null;
if (typeof closeCallback === "function") {
closeCallback.apply(undefined, originalArgs);
}
} }
that._hideNativeModalView(parent, whenClosedCallback);
} }
}; };
} }
protected _hideNativeModalView(parent: ViewCommon) { protected abstract _hideNativeModalView(parent: ViewCommon, whenClosedCallback: () => void);
//
}
protected _raiseLayoutChangedEvent() { protected _raiseLayoutChangedEvent() {
const args: EventData = { const args: EventData = {

View File

@ -613,14 +613,14 @@ export class View extends ViewCommon {
this._dialogFragment.show(parent._getRootFragmentManager(), this._domId.toString()); this._dialogFragment.show(parent._getRootFragmentManager(), this._domId.toString());
} }
protected _hideNativeModalView(parent: View) { protected _hideNativeModalView(parent: View, whenClosedCallback: () => void) {
const manager = this._dialogFragment.getFragmentManager(); const manager = this._dialogFragment.getFragmentManager();
if (manager) { if (manager) {
this._dialogFragment.dismissAllowingStateLoss(); this._dialogFragment.dismissAllowingStateLoss();
} }
this._dialogFragment = null; this._dialogFragment = null;
super._hideNativeModalView(parent); whenClosedCallback();
} }
[isEnabledProperty.setNative](value: boolean) { [isEnabledProperty.setNative](value: boolean) {

View File

@ -380,8 +380,14 @@ export class View extends ViewCommon {
} }
const parentController = parentWithController.viewController; const parentController = parentWithController.viewController;
if (parentController.presentedViewController) {
traceWrite("Parent is already presenting view controller. Close the current modal page before showing another one!",
traceCategories.ViewHierarchy, traceMessageType.error);
return;
}
if (!parentController.view || !parentController.view.window) { if (!parentController.view || !parentController.view.window) {
traceWrite("Parent page is not part of the window hierarchy. Close the current modal page before showing another one!", traceWrite("Parent page is not part of the window hierarchy.",
traceCategories.ViewHierarchy, traceMessageType.error); traceCategories.ViewHierarchy, traceMessageType.error);
return; return;
} }
@ -426,7 +432,7 @@ export class View extends ViewCommon {
} }
} }
protected _hideNativeModalView(parent: View) { protected _hideNativeModalView(parent: View, whenClosedCallback: () => void) {
if (!parent || !parent.viewController) { if (!parent || !parent.viewController) {
traceError("Trying to hide modal view but no parent with viewController specified.") traceError("Trying to hide modal view but no parent with viewController specified.")
return; return;
@ -435,8 +441,7 @@ export class View extends ViewCommon {
const parentController = parent.viewController; const parentController = parent.viewController;
const animated = (<any>this.viewController).animated; const animated = (<any>this.viewController).animated;
super._hideNativeModalView(parent); parentController.dismissViewControllerAnimatedCompletion(animated, whenClosedCallback);
parentController.dismissModalViewControllerAnimated(animated);
} }
[isEnabledProperty.getDefault](): boolean { [isEnabledProperty.getDefault](): boolean {
@ -907,11 +912,11 @@ export namespace ios {
const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top; const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top;
const currentInsetsTop = this.view.safeAreaInsets.top; const currentInsetsTop = this.view.safeAreaInsets.top;
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0); const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom; const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom;
const currentInsetsBottom = this.view.safeAreaInsets.bottom; const currentInsetsBottom = this.view.safeAreaInsets.bottom;
const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0); const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0);
if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) { if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) {
const additionalInsets = new UIEdgeInsets({ top: additionalInsetsTop, left: 0, bottom: additionalInsetsBottom, right: 0 }); const additionalInsets = new UIEdgeInsets({ top: additionalInsetsTop, left: 0, bottom: additionalInsetsBottom, right: 0 });
this.additionalSafeAreaInsets = additionalInsets; this.additionalSafeAreaInsets = additionalInsets;