feat(frame): handle back navigation when common layout is used as a root element (#5608)

* test(e2e): update modal navigation app

Add layout as root.
Add show modal layout.

* chore(frame): move frame stack modifiers in a separate frame-stack module

* feat(frame): handle back navigation when using common layout as root element
This commit is contained in:
Alexander Djenkov
2018-04-02 22:27:44 +03:00
committed by Svetoslav
parent 8a1958e82e
commit 70f01123df
12 changed files with 161 additions and 48 deletions

View File

@ -3,3 +3,4 @@ import * as application from "tns-core-modules/application";
application.run({ moduleName: "app-root" }); application.run({ moduleName: "app-root" });
// application.run({ moduleName: "tab-root" }); // application.run({ moduleName: "tab-root" });
// application.run({ moduleName: "layout-root" });

View File

@ -41,6 +41,14 @@ export function onModalPage(args: EventData) {
false); false);
} }
export function onModalLayout(args: EventData) {
const view = args.object as View;
view.showModal("modal-layout/modal-layout",
"context",
() => console.log("home-page modal layout closed"),
false);
}
export function onModalTabView(args: EventData) { export function onModalTabView(args: EventData) {
const fullscreen = false; const fullscreen = false;
const animated = false; const animated = false;
@ -61,7 +69,14 @@ export function onNavigate(args: EventData) {
page.frame.navigate("second/second-page"); page.frame.navigate("second/second-page");
} }
export function onRootViewChange() { export function onFrameRootViewReset() {
let rootView = application.getRootView(); application._resetRootView({ moduleName: "app-root" });
rootView instanceof Frame ? application._resetRootView({ moduleName: "tab-root" }) : application._resetRootView({ moduleName: "app-root" });
} }
export function onTabRootViewReset() {
application._resetRootView({ moduleName: "tab-root" });
}
export function onLayoutRootViewReset() {
application._resetRootView({ moduleName: "layout-root" });
}

View File

@ -12,8 +12,11 @@
<StackLayout> <StackLayout>
<Button text="Show Modal Page With Frame" tap="onModalFrame" /> <Button text="Show Modal Page With Frame" tap="onModalFrame" />
<Button text="Show Modal Page" tap="onModalPage" /> <Button text="Show Modal Page" tap="onModalPage" />
<Button text="Show Modal Layout" tap="onModalLayout" />
<Button text="Show Modal TabView" tap="onModalTabView" /> <Button text="Show Modal TabView" tap="onModalTabView" />
<Button text="Navigate To Second Page" tap="onNavigate" /> <Button text="Navigate To Second Page" tap="onNavigate" />
<Button text="Change Root View" tap="onRootViewChange" /> <Button text="Reset Frame Root View" tap="onFrameRootViewReset" />
<Button text="Reset Tab Root View" tap="onTabRootViewReset" />
<Button text="Reset Layout Root View" tap="onLayoutRootViewReset" />
</StackLayout> </StackLayout>
</Page> </Page>

View File

@ -0,0 +1,3 @@
<GridLayout>
<Frame defaultPage="home/home-page" />
</GridLayout>

View File

@ -0,0 +1,23 @@
export function onShowingModally() {
console.log("modal-layout showingModally");
}
export function onLoaded() {
console.log("modal-layout loaded");
}
export function onNavigatingTo() {
console.log("modal-layout onNavigatingTo");
}
export function onNavigatingFrom() {
console.log("modal-layout onNavigatingFrom");
}
export function onNavigatedTo() {
console.log("modal-layout onNavigatedTo");
}
export function onNavigatedFrom() {
console.log("modal-layout onNavigatedFrom");
}

View File

@ -0,0 +1,7 @@
<GridLayout backgroundColor="green" showingModally="onShowingModally" loaded="onLoaded"
navigatingTo="onNavigatingTo"
navigatingFrom="onNavigatingFrom"
navigatedTo="onNavigatedTo"
navigatedFrom="onNavigatedFrom">
<Frame defaultPage="modal/modal-page"></Frame>
</GridLayout>

View File

@ -12,7 +12,9 @@ const modalFrame = "Show Modal Page With Frame";
const modalPage = "Show Modal Page"; const modalPage = "Show Modal Page";
const modalTabView = "Show Modal TabView"; const modalTabView = "Show Modal TabView";
const navToSecondPage = "Navigate To Second Page"; const navToSecondPage = "Navigate To Second Page";
const rootView = "Change Root View"; const resetFrameRootView = "Reset Frame Root View";
const resetTabRootView = "Reset Tab Root View";
const resetLayoutRootView = "Reset Layout Root View";
const showNestedModalFrame = "Show Nested Modal Page With Frame"; const showNestedModalFrame = "Show Nested Modal Page With Frame";
const showNestedModalPage = "Show Nested Modal Page"; const showNestedModalPage = "Show Nested Modal Page";
@ -35,9 +37,19 @@ export class Screen {
console.log(home + " loaded!"); console.log(home + " loaded!");
} }
changeRootView = async () => { resetFrameRootView = async () => {
const btnChangeRootView = await this._driver.findElementByText(rootView); const btnResetFrameRootView = await this._driver.findElementByText(resetFrameRootView);
await btnChangeRootView.tap(); await btnResetFrameRootView.tap();
}
resetTabRootView = async () => {
const btnResetTabRootView = await this._driver.findElementByText(resetTabRootView);
await btnResetTabRootView.tap();
}
resetLayoutRootView = async () => {
const btnResetLayoutRootView = await this._driver.findElementByText(resetLayoutRootView);
await btnResetLayoutRootView.tap();
} }
loadedTabRootView = async () => { loadedTabRootView = async () => {
@ -52,7 +64,7 @@ export class Screen {
try { try {
await this.loadedTabRootView(); await this.loadedTabRootView();
} catch (err) { } catch (err) {
await this.changeRootView(); await this.resetTabRootView();
await this.loadedTabRootView(); await this.loadedTabRootView();
} }
} }

View File

@ -263,7 +263,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
that._closeModalCallback = null; that._closeModalCallback = null;
that._dialogClosed(); that._dialogClosed();
parent._modal = null; parent._modal = null;
if (typeof closeCallback === "function") { if (typeof closeCallback === "function") {
closeCallback.apply(undefined, arguments); closeCallback.apply(undefined, arguments);
} }
@ -976,6 +976,18 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
_onDetachedFromWindow(): void { _onDetachedFromWindow(): void {
// //
} }
_hasAncestorView(ancestorView: ViewDefinition): boolean {
let matcher = (view: ViewDefinition) => view === ancestorView;
for (let parent = this.parent; parent != null; parent = parent.parent) {
if (matcher(<ViewDefinition>parent)) {
return true;
}
}
return false;
}
} }
export const automationTextProperty = new Property<ViewCommon, string>({ name: "automationText" }); export const automationTextProperty = new Property<ViewCommon, string>({ name: "automationText" });

View File

@ -22,6 +22,7 @@ import {
import { Background, ad as androidBackground } from "../../styling/background"; import { Background, ad as androidBackground } from "../../styling/background";
import { profile } from "../../../profiling"; import { profile } from "../../../profiling";
import { topmost } from "../../frame/frame-stack";
export * from "./view-common"; export * from "./view-common";
@ -287,6 +288,18 @@ export class View extends ViewCommon {
super.onUnloaded(); super.onUnloaded();
} }
public onBackPressed(): boolean {
let topmostFrame = topmost();
// Delegate back navigation handling to the topmost Frame
// when it's a child of the current View.
if (topmostFrame && topmostFrame._hasAncestorView(this)) {
return topmostFrame.onBackPressed();
}
return false;
}
private hasGestureObservers() { private hasGestureObservers() {
return this._gestureObservers && Object.keys(this._gestureObservers).length > 0 return this._gestureObservers && Object.keys(this._gestureObservers).length > 0
} }

View File

@ -110,7 +110,7 @@ export abstract class View extends ViewBase {
* String value used when hooking to shownModally event. * String value used when hooking to shownModally event.
*/ */
public static shownModallyEvent: string; public static shownModallyEvent: string;
/** /**
* Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform. * Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform.
*/ */
@ -705,6 +705,11 @@ export abstract class View extends ViewBase {
* Called in android when native view is dettached from window. * Called in android when native view is dettached from window.
*/ */
_onDetachedFromWindow(): void; _onDetachedFromWindow(): void;
/**
* Checks whether the current view has specific view for an ancestor.
*/
_hasAncestorView(ancestorView: View): boolean;
//@endprivate //@endprivate
/** /**
@ -797,7 +802,7 @@ export const isEnabledProperty: Property<View, boolean>;
export const isUserInteractionEnabledProperty: Property<View, boolean>; export const isUserInteractionEnabledProperty: Property<View, boolean>;
export namespace ios { export namespace ios {
export function isContentScrollable(controller: any /* UIViewController */, owner: View): boolean export function isContentScrollable(controller: any /* UIViewController */, owner: View): boolean
export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void
export function updateConstraints(controller: any /* UIViewController */, owner: View): void; export function updateConstraints(controller: any /* UIViewController */, owner: View): void;
export function layoutView(controller: any /* UIViewController */, owner: View): void; export function layoutView(controller: any /* UIViewController */, owner: View): void;

View File

@ -9,10 +9,9 @@ import { knownFolders, path } from "../../file-system";
import { parse, createViewFromEntry } from "../builder"; import { parse, createViewFromEntry } from "../builder";
import { profile } from "../../profiling"; import { profile } from "../../profiling";
import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from "./frame-stack";
export * from "../core/view"; export * from "../core/view";
let frameStack: Array<FrameBase> = [];
function buildEntryFromArgs(arg: any): NavigationEntry { function buildEntryFromArgs(arg: any): NavigationEntry {
let entry: NavigationEntry; let entry: NavigationEntry;
if (typeof arg === "string") { if (typeof arg === "string") {
@ -403,41 +402,15 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition {
} }
public _pushInFrameStack() { public _pushInFrameStack() {
if (this._isInFrameStack && frameStack[frameStack.length - 1] === this) { _pushInFrameStack(this);
return;
}
if (this._isInFrameStack) {
const indexOfFrame = frameStack.indexOf(this);
frameStack.splice(indexOfFrame, 1);
}
frameStack.push(this);
this._isInFrameStack = true;
} }
public _popFromFrameStack() { public _popFromFrameStack() {
if (!this._isInFrameStack) { _popFromFrameStack(this);
return;
}
const top = topmost();
if (top !== this) {
throw new Error("Cannot pop a Frame which is not at the top of the navigation stack.");
}
frameStack.pop();
this._isInFrameStack = false;
} }
public _removeFromFrameStack() { public _removeFromFrameStack() {
if (!this._isInFrameStack) { _removeFromFrameStack(this);
return;
}
const index = frameStack.indexOf(this);
frameStack.splice(index, 1);
this._isInFrameStack = false;
} }
public _dialogClosed(): void { public _dialogClosed(): void {
@ -585,11 +558,7 @@ export function getFrameById(id: string): FrameBase {
} }
export function topmost(): FrameBase { export function topmost(): FrameBase {
if (frameStack.length > 0) { return frameStackTopmost();
return frameStack[frameStack.length - 1];
}
return undefined;
} }
export function goBack(): boolean { export function goBack(): boolean {

View File

@ -0,0 +1,50 @@
// Types.
import { FrameBase } from "./frame-common";
export let frameStack: Array<FrameBase> = [];
export function topmost(): FrameBase {
if (frameStack.length > 0) {
return frameStack[frameStack.length - 1];
}
return undefined;
}
export function _pushInFrameStack(frame: FrameBase): void {
if (frame._isInFrameStack && frameStack[frameStack.length - 1] === frame) {
return;
}
if (frame._isInFrameStack) {
const indexOfFrame = frameStack.indexOf(frame);
frameStack.splice(indexOfFrame, 1);
}
frameStack.push(frame);
frame._isInFrameStack = true;
}
export function _popFromFrameStack(frame: FrameBase): void {
if (!frame._isInFrameStack) {
return;
}
const top = topmost();
if (top !== frame) {
throw new Error("Cannot pop a Frame which is not at the top of the navigation stack.");
}
frameStack.pop();
frame._isInFrameStack = false;
}
export function _removeFromFrameStack(frame: FrameBase): void {
if (!frame._isInFrameStack) {
return;
}
const index = frameStack.indexOf(frame);
frameStack.splice(index, 1);
frame._isInFrameStack = false;
}