mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-16 11:42:04 +08:00
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:

committed by
Svetoslav

parent
8a1958e82e
commit
70f01123df
@ -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" });
|
||||||
|
@ -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" });
|
||||||
|
}
|
@ -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>
|
||||||
|
3
e2e/modal-navigation/app/layout-root.xml
Normal file
3
e2e/modal-navigation/app/layout-root.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<GridLayout>
|
||||||
|
<Frame defaultPage="home/home-page" />
|
||||||
|
</GridLayout>
|
23
e2e/modal-navigation/app/modal-layout/modal-layout.ts
Normal file
23
e2e/modal-navigation/app/modal-layout/modal-layout.ts
Normal 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");
|
||||||
|
}
|
7
e2e/modal-navigation/app/modal-layout/modal-layout.xml
Normal file
7
e2e/modal-navigation/app/modal-layout/modal-layout.xml
Normal 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>
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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" });
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
9
tns-core-modules/ui/core/view/view.d.ts
vendored
9
tns-core-modules/ui/core/view/view.d.ts
vendored
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
50
tns-core-modules/ui/frame/frame-stack.ts
Normal file
50
tns-core-modules/ui/frame/frame-stack.ts
Normal 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;
|
||||||
|
}
|
Reference in New Issue
Block a user