@ng-native/router 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/discard-failed-entries.d.ts +11 -0
- package/dist/discard-failed-entries.js +48 -0
- package/dist/discard-failed-entries.js.map +1 -0
- package/dist/in-tab.d.ts +9 -1
- package/dist/in-tab.js +2 -0
- package/dist/in-tab.js.map +1 -1
- package/dist/native-back.d.ts +21 -1
- package/dist/native-back.js +94 -3
- package/dist/native-back.js.map +1 -1
- package/dist/native-header.d.ts +8 -1
- package/dist/native-header.js +18 -4
- package/dist/native-header.js.map +1 -1
- package/dist/native-navigation.d.ts +10 -2
- package/dist/native-navigation.js +45 -9
- package/dist/native-navigation.js.map +1 -1
- package/dist/native-platform-location.d.ts +42 -0
- package/dist/native-platform-location.js +66 -0
- package/dist/native-platform-location.js.map +1 -1
- package/dist/native-stack-outlet.d.ts +83 -0
- package/dist/native-stack-outlet.js +245 -19
- package/dist/native-stack-outlet.js.map +1 -1
- package/dist/native-stack-reuse-strategy.d.ts +34 -0
- package/dist/native-stack-reuse-strategy.js +119 -7
- package/dist/native-stack-reuse-strategy.js.map +1 -1
- package/dist/native-tab.d.ts +8 -6
- package/dist/native-tab.js +23 -2
- package/dist/native-tab.js.map +1 -1
- package/dist/native-tabs-outlet.d.ts +9 -3
- package/dist/native-tabs-outlet.js +21 -4
- package/dist/native-tabs-outlet.js.map +1 -1
- package/dist/popping-to.d.ts +8 -0
- package/dist/popping-to.js +2 -0
- package/dist/popping-to.js.map +1 -0
- package/dist/presented-route.js +12 -10
- package/dist/presented-route.js.map +1 -1
- package/dist/provide-native-router.js +4 -0
- package/dist/provide-native-router.js.map +1 -1
- package/dist/screen-header.d.ts +5 -3
- package/dist/screen-header.js +5 -3
- package/dist/screen-header.js.map +1 -1
- package/dist/screen-presentation.d.ts +5 -0
- package/dist/screen-presentation.js.map +1 -1
- package/dist/screen-safe-area-view.d.ts +6 -1
- package/dist/screen-safe-area-view.js +12 -4
- package/dist/screen-safe-area-view.js.map +1 -1
- package/dist/screens-version.d.ts +12 -0
- package/dist/screens-version.js +20 -0
- package/dist/screens-version.js.map +1 -0
- package/dist/screens.js +3 -0
- package/dist/screens.js.map +1 -1
- package/dist/url-commands.d.ts +24 -0
- package/dist/url-commands.js +102 -0
- package/dist/url-commands.js.map +1 -0
- package/package.json +3 -3
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
import { type ComponentRef, type EnvironmentInjector, type OnInit } from '@angular/core';
|
|
20
20
|
import { ActivatedRoute, type Data, type RouterOutletContract } from '@angular/router';
|
|
21
21
|
import * as i0 from "@angular/core";
|
|
22
|
+
interface Rewind {
|
|
23
|
+
navigation: number;
|
|
24
|
+
steps: number;
|
|
25
|
+
/** The url the screen gone back to showed when it was covered, if known. */
|
|
26
|
+
url?: string;
|
|
27
|
+
}
|
|
28
|
+
/** The rewind a router's navigation in flight is waiting to make, if any. Exposed for tests. */
|
|
29
|
+
export declare function pendingRewind(router: object): Rewind | undefined;
|
|
22
30
|
export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
23
31
|
/**
|
|
24
32
|
* Named outlets work exactly as on the web: `<native-stack-outlet name="list" />` is filled by
|
|
@@ -39,6 +47,7 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
39
47
|
* `RouterOutletContract` directly, which is how its own tests drive it.
|
|
40
48
|
*/
|
|
41
49
|
private readonly router;
|
|
50
|
+
private readonly routerConfig;
|
|
42
51
|
/** For a refused back to reach the screen as native's own report would. */
|
|
43
52
|
private readonly engine;
|
|
44
53
|
private readonly errors;
|
|
@@ -49,6 +58,8 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
49
58
|
private held;
|
|
50
59
|
private readonly statusBar;
|
|
51
60
|
private readonly navigationBar;
|
|
61
|
+
/** iOS 26 and later, where a screen runs under its navigation bar: see `insetContent`. */
|
|
62
|
+
private readonly underBar;
|
|
52
63
|
/** Both bars' claims, as the screens carry them. */
|
|
53
64
|
private readonly systemBars;
|
|
54
65
|
private readonly entries;
|
|
@@ -57,6 +68,11 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
57
68
|
* succeeds: one that fails leaves the stack as it was.
|
|
58
69
|
*/
|
|
59
70
|
private superseded;
|
|
71
|
+
/**
|
|
72
|
+
* The screens a navigation back to `kept`, further down, took off the stack, kept until it
|
|
73
|
+
* succeeds as superseded ones are: one that fails puts them back, and `kept` as it was.
|
|
74
|
+
*/
|
|
75
|
+
private popped;
|
|
60
76
|
protected readonly fill: {
|
|
61
77
|
flex: number;
|
|
62
78
|
};
|
|
@@ -170,6 +186,35 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
170
186
|
* superseded goes at once.
|
|
171
187
|
*/
|
|
172
188
|
private supersede;
|
|
189
|
+
/**
|
|
190
|
+
* Take `above` off the stack for a navigation back to `kept`: at once when there is no navigation
|
|
191
|
+
* to wait for, and otherwise once it has succeeded. They stay on the native stack until then, and
|
|
192
|
+
* the navigation ends before anything commits, so native sees one change or none.
|
|
193
|
+
*/
|
|
194
|
+
private pop;
|
|
195
|
+
/** Take out for good what a navigation that succeeded popped. */
|
|
196
|
+
private dropPopped;
|
|
197
|
+
/** Put back what a navigation that failed popped, and the screen it went back to as it was. */
|
|
198
|
+
private unpop;
|
|
199
|
+
/** The history entries the navigation in flight adds: none where it skips or replaces one. */
|
|
200
|
+
private entriesAdded;
|
|
201
|
+
/**
|
|
202
|
+
* A navigation the history did not drive went back to a screen kept further down, taking
|
|
203
|
+
* `dropped` screens off: a back has already moved the history, and a navigation that skips the
|
|
204
|
+
* location change leaves it where it is. `url` is the one the screen gone back to was showing.
|
|
205
|
+
*/
|
|
206
|
+
private rewindAfter;
|
|
207
|
+
/**
|
|
208
|
+
* Back to the kept screen's own entry, when it is the one that far back, with no popstate: the
|
|
209
|
+
* router is there already. The entry keeps its state, the screen's presentation with it, and
|
|
210
|
+
* takes the navigation's id, as the router writes one into the entry a back reaches. A stack in
|
|
211
|
+
* a tab whose history went through another tab has that tab's entry there, and is left as it is.
|
|
212
|
+
*
|
|
213
|
+
* A navigation to another page under the kept screen, such as a step in a stack inside it, finds
|
|
214
|
+
* the url that screen was showing there instead. The page's own entry stays, and the entries of
|
|
215
|
+
* the screens taken off between the two go.
|
|
216
|
+
*/
|
|
217
|
+
private rewind;
|
|
173
218
|
/** A navigation succeeded: what it superseded goes, and what it did is no longer undoable. */
|
|
174
219
|
private settle;
|
|
175
220
|
/**
|
|
@@ -180,6 +225,12 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
180
225
|
* stack inside it. That one goes, with everything in it, and the screen it covered carries on.
|
|
181
226
|
*/
|
|
182
227
|
private rollBack;
|
|
228
|
+
/**
|
|
229
|
+
* Give the router's context for this outlet back the outlet contexts of the top screen, which the
|
|
230
|
+
* navigation took when it covered that screen: the next navigation finds the outlets inside the
|
|
231
|
+
* screen through them.
|
|
232
|
+
*/
|
|
233
|
+
private recover;
|
|
183
234
|
/**
|
|
184
235
|
* Give the top screen back its change detection when the router covered it for a navigation
|
|
185
236
|
* that is not going to happen: the one given, or the one in progress.
|
|
@@ -191,6 +242,22 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
191
242
|
* the same merge a page's own host style gets, just asserted last so the stack's layout wins.
|
|
192
243
|
*/
|
|
193
244
|
private fillScreen;
|
|
245
|
+
/**
|
|
246
|
+
* On iOS 26 and later, the view a page's content commits in, as react-native-screens'
|
|
247
|
+
* `ScreenStackItem` wraps it in its `SafeAreaView` there. From 4.30 the screen runs under the
|
|
248
|
+
* navigation bar whether the bar is opaque or not
|
|
249
|
+
* (`+[RNSScreenStackHeaderConfig screensExtendUnderOpaqueNavigationBar]`), and this view insets
|
|
250
|
+
* the content below an opaque one. Before 4.30 the screen starts below such a bar, its safe area
|
|
251
|
+
* is zero at the top, and the view insets by nothing. Null elsewhere: the content commits
|
|
252
|
+
* straight into the screen.
|
|
253
|
+
*/
|
|
254
|
+
private insetContent;
|
|
255
|
+
/**
|
|
256
|
+
* The top edge while the page's header is shown and not translucent, which a large title on iOS
|
|
257
|
+
* is, and no edge otherwise, as `getSafeAreaEdges` in `ScreenStackItem` chooses. The view stays
|
|
258
|
+
* when the header changes, so the content is not made again. Native takes all four edges.
|
|
259
|
+
*/
|
|
260
|
+
private followHeader;
|
|
194
261
|
private createScreen;
|
|
195
262
|
/**
|
|
196
263
|
* Puts the app's status bar and navigation bar claims on a screen that has no value of its own
|
|
@@ -228,6 +295,13 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
228
295
|
* way, through the engine, as if native had sent it.
|
|
229
296
|
*/
|
|
230
297
|
private refusesButton;
|
|
298
|
+
/**
|
|
299
|
+
* Android's header back arrow on `screen`: refused where the screen refuses, as Back is, and
|
|
300
|
+
* otherwise a step back, or the presented page dismissed when the screen is that page's.
|
|
301
|
+
* react-native-screens sends the arrow of a stack's first screen on the screen the stack sits
|
|
302
|
+
* in, which holds the top screen's refusal for it.
|
|
303
|
+
*/
|
|
304
|
+
private onHeaderBack;
|
|
231
305
|
/** Refuses Android's Back for a screen that sets `preventNativeDismiss`, and tells its page. */
|
|
232
306
|
private refuseBack;
|
|
233
307
|
/**
|
|
@@ -256,8 +330,16 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
256
330
|
* `popBy` does when a tab's history interleaves.
|
|
257
331
|
*/
|
|
258
332
|
private popToEntry;
|
|
333
|
+
/**
|
|
334
|
+
* Send the router to `entry` by a navigation that replaces the entry, for a pop the history
|
|
335
|
+
* behind cannot make. With the same url on the stack twice, the reuse strategy is told which of
|
|
336
|
+
* the screens at it this is.
|
|
337
|
+
*/
|
|
338
|
+
private navigateDownTo;
|
|
259
339
|
/** Whether the navigation that happens next succeeds. */
|
|
260
340
|
private nextNavigation;
|
|
341
|
+
/** The url the app showed before the navigation in flight, as the router writes it. */
|
|
342
|
+
private previousUrl;
|
|
261
343
|
/** The url a screen was showing when it was covered, rebuilt from its own route snapshot. */
|
|
262
344
|
private urlOf;
|
|
263
345
|
/**
|
|
@@ -276,3 +358,4 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
276
358
|
static ɵfac: i0.ɵɵFactoryDeclaration<NativeStackOutlet, never>;
|
|
277
359
|
static ɵcmp: i0.ɵɵComponentDeclaration<NativeStackOutlet, "native-stack-outlet", never, {}, {}, never, never, true, never>;
|
|
278
360
|
}
|
|
361
|
+
export {};
|
|
@@ -17,19 +17,20 @@
|
|
|
17
17
|
* (tabs), where every screen is mounted at once.
|
|
18
18
|
*/
|
|
19
19
|
import { ApplicationRef, Component, DestroyRef, ElementRef, ErrorHandler, HostAttributeToken, InjectionToken, Injector, Renderer2, afterEveryRender, computed, createComponent, effect, inject, signal, untracked, } from '@angular/core';
|
|
20
|
-
import { ActivatedRoute, ChildrenOutletContexts, NavigationCancel, NavigationEnd, NavigationError, PRIMARY_OUTLET, Router, createUrlTreeFromSnapshot, } from '@angular/router';
|
|
20
|
+
import { ActivatedRoute, ChildrenOutletContexts, NavigationCancel, NavigationEnd, NavigationError, PRIMARY_OUTLET, ROUTER_CONFIGURATION, Router, createUrlTreeFromSnapshot, } from '@angular/router';
|
|
21
21
|
import { Location, PlatformLocation } from '@angular/common';
|
|
22
|
-
import { NavigationBar, SCREEN_IN_FRONT, StatusBar } from '@ng-native/device';
|
|
22
|
+
import { NavigationBar, OS_VERSION, SCREEN_IN_FRONT, StatusBar } from '@ng-native/device';
|
|
23
23
|
import { bindRouteInputs } from "./bind-route-inputs.js";
|
|
24
|
-
import { NativeBack, isShowing } from "./native-back.js";
|
|
24
|
+
import { NativeBack, isShowing, restoreSnapshot } from "./native-back.js";
|
|
25
25
|
import { navigationIntent } from "./native-navigation.js";
|
|
26
26
|
import { NativePlatformLocation } from "./native-platform-location.js";
|
|
27
27
|
import { PRESENTED, withoutPresented } from "./presented-route.js";
|
|
28
|
+
import { poppingTo } from "./popping-to.js";
|
|
28
29
|
import { markScreenRoute } from "./tab-routes.js";
|
|
29
30
|
import { IN_TAB } from "./in-tab.js";
|
|
30
31
|
import { ownHost } from "./own-host.js";
|
|
31
32
|
import { SCREEN_HEADER } from "./screen-header.js";
|
|
32
|
-
import { HostEngine } from '@ng-native/fabric';
|
|
33
|
+
import { HostEngine, nativePlatform } from '@ng-native/fabric';
|
|
33
34
|
import { ActivityState } from "./screens.js";
|
|
34
35
|
import * as i0 from "@angular/core";
|
|
35
36
|
/**
|
|
@@ -41,6 +42,16 @@ import * as i0 from "@angular/core";
|
|
|
41
42
|
* push inside that stack must see it as a pushed screen, not one to present over.
|
|
42
43
|
*/
|
|
43
44
|
const presented = new WeakMap();
|
|
45
|
+
/**
|
|
46
|
+
* The history entries, per router, that the navigation in flight took screens off for. A
|
|
47
|
+
* navigation back to a screen holding a stack of its own drops screens in both stacks, and the
|
|
48
|
+
* rewind to its entry goes back over all of them, once.
|
|
49
|
+
*/
|
|
50
|
+
const rewinds = new WeakMap();
|
|
51
|
+
/** The rewind a router's navigation in flight is waiting to make, if any. Exposed for tests. */
|
|
52
|
+
export function pendingRewind(router) {
|
|
53
|
+
return rewinds.get(router);
|
|
54
|
+
}
|
|
44
55
|
/** What every screen needs to fill the stack, the same five keys RNSScreen itself expects. */
|
|
45
56
|
const FILL = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 };
|
|
46
57
|
/** A swipe-back area with no limit on any edge. */
|
|
@@ -79,6 +90,7 @@ export class NativeStackOutlet {
|
|
|
79
90
|
* `RouterOutletContract` directly, which is how its own tests drive it.
|
|
80
91
|
*/
|
|
81
92
|
router = inject(Router, { optional: true });
|
|
93
|
+
routerConfig = inject(ROUTER_CONFIGURATION, { optional: true });
|
|
82
94
|
/** For a refused back to reach the screen as native's own report would. */
|
|
83
95
|
engine = inject(HostEngine, { optional: true });
|
|
84
96
|
errors = inject(ErrorHandler);
|
|
@@ -89,6 +101,8 @@ export class NativeStackOutlet {
|
|
|
89
101
|
held = null;
|
|
90
102
|
statusBar = inject(StatusBar);
|
|
91
103
|
navigationBar = inject(NavigationBar);
|
|
104
|
+
/** iOS 26 and later, where a screen runs under its navigation bar: see `insetContent`. */
|
|
105
|
+
underBar = nativePlatform() === 'ios' && (inject(OS_VERSION) ?? 0) >= 26;
|
|
92
106
|
/** Both bars' claims, as the screens carry them. */
|
|
93
107
|
systemBars = computed(() => ({
|
|
94
108
|
...this.statusBar.forScreens(),
|
|
@@ -101,6 +115,11 @@ export class NativeStackOutlet {
|
|
|
101
115
|
* succeeds: one that fails leaves the stack as it was.
|
|
102
116
|
*/
|
|
103
117
|
superseded = null;
|
|
118
|
+
/**
|
|
119
|
+
* The screens a navigation back to `kept`, further down, took off the stack, kept until it
|
|
120
|
+
* succeeds as superseded ones are: one that fails puts them back, and `kept` as it was.
|
|
121
|
+
*/
|
|
122
|
+
popped = null;
|
|
104
123
|
fill = { flex: 1 };
|
|
105
124
|
constructor() {
|
|
106
125
|
ownHost(this.host.nativeElement);
|
|
@@ -110,11 +129,8 @@ export class NativeStackOutlet {
|
|
|
110
129
|
// Android's back button, and `NativeNavigation.back()`. Declining at the bottom of the stack
|
|
111
130
|
// is deliberate: the outlet this one sits in gets its turn, and when none takes it the
|
|
112
131
|
// platform backgrounds the app, which is what a user expects, rather than the app swallowing
|
|
113
|
-
// the press and appearing stuck.
|
|
114
|
-
//
|
|
115
|
-
// Not yet verified on a device: whether react-native-screens' own Android back handling
|
|
116
|
-
// (`nativeBackButtonDismissalEnabled`) also pops the screen, which would make one press pop
|
|
117
|
-
// twice. If a back press ever skips a screen on Android, look here first.
|
|
132
|
+
// the press and appearing stuck. react-native-screens' Android stack leaves the button to JS;
|
|
133
|
+
// its header's back arrow comes through `onHeaderBack`.
|
|
118
134
|
const back = this.back;
|
|
119
135
|
const unsubscribeBack = back.handle((from) => from === 'button' && this.refusesButton() ? true : this.goBack());
|
|
120
136
|
const unsubscribeStack = back.addStack({
|
|
@@ -134,8 +150,10 @@ export class NativeStackOutlet {
|
|
|
134
150
|
});
|
|
135
151
|
// Optional for the same reason as the router: the outlet's own tests stand one in without it.
|
|
136
152
|
const navigations = this.router?.events?.subscribe((event) => {
|
|
137
|
-
|
|
153
|
+
// A cancel after activation is one the router does not undo: a page it created navigated.
|
|
154
|
+
if (event instanceof NavigationEnd || event instanceof NavigationCancel) {
|
|
138
155
|
this.settle(event.id);
|
|
156
|
+
}
|
|
139
157
|
else if (event instanceof NavigationError)
|
|
140
158
|
this.rollBack(event.id);
|
|
141
159
|
});
|
|
@@ -161,11 +179,12 @@ export class NativeStackOutlet {
|
|
|
161
179
|
unsubscribeBack();
|
|
162
180
|
unsubscribeStack();
|
|
163
181
|
navigations?.unsubscribe();
|
|
164
|
-
for (const entry of this.entries) {
|
|
182
|
+
for (const entry of [...this.entries, ...(this.popped?.entries ?? [])]) {
|
|
165
183
|
entry.unbind();
|
|
166
184
|
entry.ref.destroy();
|
|
167
185
|
}
|
|
168
186
|
this.entries.length = 0;
|
|
187
|
+
this.popped = null;
|
|
169
188
|
// Only while it is this outlet's: a page kept by the reuse strategy takes its outlets'
|
|
170
189
|
// registrations with it, and gets them back as it is attached, before what was over it goes.
|
|
171
190
|
if (this.parentContexts.getContext(this.name)?.outlet === this) {
|
|
@@ -360,6 +379,7 @@ export class NativeStackOutlet {
|
|
|
360
379
|
detached: false,
|
|
361
380
|
unbind,
|
|
362
381
|
presentation,
|
|
382
|
+
entries: this.entriesAdded(),
|
|
363
383
|
activatedIn: navigation,
|
|
364
384
|
inFront,
|
|
365
385
|
});
|
|
@@ -376,13 +396,18 @@ export class NativeStackOutlet {
|
|
|
376
396
|
createPage(component, route, environmentInjector, screen, inFront, outlet = this.name) {
|
|
377
397
|
let ref;
|
|
378
398
|
let unbind;
|
|
399
|
+
const header = signal(false, /* @ts-ignore */
|
|
400
|
+
...(ngDevMode ? [{ debugName: "header" }] : /* istanbul ignore next */ []));
|
|
401
|
+
const content = this.insetContent(screen);
|
|
379
402
|
try {
|
|
380
403
|
ref = createComponent(component, {
|
|
381
404
|
environmentInjector,
|
|
382
|
-
elementInjector: this.outletInjector(route, inFront, screen, outlet),
|
|
405
|
+
elementInjector: this.outletInjector(route, inFront, screen, header, outlet),
|
|
383
406
|
hostElement: screen,
|
|
384
407
|
});
|
|
385
408
|
this.applicationRef.attachView(ref.hostView);
|
|
409
|
+
if (content)
|
|
410
|
+
this.followHeader(content, header, ref.injector);
|
|
386
411
|
// Route params onto inputs before the first change detection, not after: a required input
|
|
387
412
|
// read in the template throws NG0950 if it is still unset when the page first renders.
|
|
388
413
|
unbind = this.bindInputs(ref, route);
|
|
@@ -425,6 +450,16 @@ export class NativeStackOutlet {
|
|
|
425
450
|
throw new Error('[angular-native] outlet is not activated, nothing to detach');
|
|
426
451
|
entry.detached = true;
|
|
427
452
|
entry.detachedIn = this.navigationId();
|
|
453
|
+
const context = this.parentContexts.getContext(this.name);
|
|
454
|
+
entry.covered =
|
|
455
|
+
context && entry.detachedIn !== undefined
|
|
456
|
+
? {
|
|
457
|
+
navigation: entry.detachedIn,
|
|
458
|
+
children: context.children,
|
|
459
|
+
snapshot: entry.route.snapshot,
|
|
460
|
+
url: this.previousUrl(),
|
|
461
|
+
}
|
|
462
|
+
: undefined;
|
|
428
463
|
entry.inFront.set(false);
|
|
429
464
|
// `ChangeDetectorRef.detach`, not `ApplicationRef.detachView`. The latter calls
|
|
430
465
|
// `renderDetachView` and pulls the view's nodes out of the tree, which is exactly what we
|
|
@@ -457,12 +492,13 @@ export class NativeStackOutlet {
|
|
|
457
492
|
this.applicationRef.attachView(ref.hostView);
|
|
458
493
|
}
|
|
459
494
|
else {
|
|
460
|
-
|
|
461
|
-
|
|
495
|
+
const entry = this.entries[index];
|
|
496
|
+
const above = this.entries.splice(index + 1);
|
|
497
|
+
if (above.length)
|
|
498
|
+
this.pop(entry, above);
|
|
462
499
|
if (this.currentIntent()?.stack === 'reset') {
|
|
463
500
|
this.supersede(this.entries.slice(0, index), this.navigationId());
|
|
464
501
|
}
|
|
465
|
-
const entry = this.entries[index];
|
|
466
502
|
entry.detached = false;
|
|
467
503
|
entry.inFront.set(true);
|
|
468
504
|
entry.route = route;
|
|
@@ -573,13 +609,116 @@ export class NativeStackOutlet {
|
|
|
573
609
|
}
|
|
574
610
|
this.superseded = { entries, navigation };
|
|
575
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Take `above` off the stack for a navigation back to `kept`: at once when there is no navigation
|
|
614
|
+
* to wait for, and otherwise once it has succeeded. They stay on the native stack until then, and
|
|
615
|
+
* the navigation ends before anything commits, so native sees one change or none.
|
|
616
|
+
*/
|
|
617
|
+
pop(kept, above) {
|
|
618
|
+
this.rewindAfter(above, kept.covered?.url);
|
|
619
|
+
const navigation = this.navigationId();
|
|
620
|
+
if (navigation === undefined) {
|
|
621
|
+
for (const entry of above)
|
|
622
|
+
this.drop(entry);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// The router has moved the route on to the navigation's snapshot already.
|
|
626
|
+
const snapshot = kept.covered?.snapshot ?? kept.route.snapshot;
|
|
627
|
+
this.popped = { navigation, entries: above, kept, snapshot, detachedIn: kept.detachedIn };
|
|
628
|
+
}
|
|
629
|
+
/** Take out for good what a navigation that succeeded popped. */
|
|
630
|
+
dropPopped(navigation) {
|
|
631
|
+
if (this.popped?.navigation !== navigation)
|
|
632
|
+
return;
|
|
633
|
+
for (const entry of this.popped.entries)
|
|
634
|
+
this.drop(entry);
|
|
635
|
+
this.popped = null;
|
|
636
|
+
}
|
|
637
|
+
/** Put back what a navigation that failed popped, and the screen it went back to as it was. */
|
|
638
|
+
unpop(navigation) {
|
|
639
|
+
const popped = this.popped;
|
|
640
|
+
if (popped?.navigation !== navigation)
|
|
641
|
+
return;
|
|
642
|
+
this.popped = null;
|
|
643
|
+
const { kept } = popped;
|
|
644
|
+
restoreSnapshot(kept.route, popped.snapshot);
|
|
645
|
+
kept.detached = true;
|
|
646
|
+
kept.detachedIn = popped.detachedIn;
|
|
647
|
+
kept.inFront.set(false);
|
|
648
|
+
kept.ref.hostView.detach();
|
|
649
|
+
this.entries.push(...popped.entries);
|
|
650
|
+
}
|
|
651
|
+
/** The history entries the navigation in flight adds: none where it skips or replaces one. */
|
|
652
|
+
entriesAdded() {
|
|
653
|
+
const extras = this.router?.getCurrentNavigation()?.extras;
|
|
654
|
+
return extras?.skipLocationChange || extras?.replaceUrl ? 0 : 1;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* A navigation the history did not drive went back to a screen kept further down, taking
|
|
658
|
+
* `dropped` screens off: a back has already moved the history, and a navigation that skips the
|
|
659
|
+
* location change leaves it where it is. `url` is the one the screen gone back to was showing.
|
|
660
|
+
*/
|
|
661
|
+
rewindAfter(dropped, url) {
|
|
662
|
+
const navigation = this.router?.getCurrentNavigation();
|
|
663
|
+
if (!navigation || navigation.trigger === 'popstate' || navigation.extras.skipLocationChange) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// A router that resolves a cancelled navigation by computing how far to go in history counts
|
|
667
|
+
// its own entries, and a rewind it did not make would put that count out of step.
|
|
668
|
+
if (this.routerConfig?.canceledNavigationResolution === 'computed')
|
|
669
|
+
return;
|
|
670
|
+
// A screen restored from another outlet has no count here, and is taken to have added one.
|
|
671
|
+
const added = dropped.reduce((total, entry) => total + (entry.entries ?? 1), 0);
|
|
672
|
+
const pending = rewinds.get(this.router);
|
|
673
|
+
// An outlet inside the screen gone back to activates after the one holding it, so the last
|
|
674
|
+
// url is the innermost screen's, the one whose entry is furthest back.
|
|
675
|
+
if (pending?.navigation === navigation.id) {
|
|
676
|
+
pending.steps += added;
|
|
677
|
+
pending.url = url;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
const steps = added + this.entriesAdded();
|
|
681
|
+
rewinds.set(this.router, { navigation: navigation.id, steps, url });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Back to the kept screen's own entry, when it is the one that far back, with no popstate: the
|
|
686
|
+
* router is there already. The entry keeps its state, the screen's presentation with it, and
|
|
687
|
+
* takes the navigation's id, as the router writes one into the entry a back reaches. A stack in
|
|
688
|
+
* a tab whose history went through another tab has that tab's entry there, and is left as it is.
|
|
689
|
+
*
|
|
690
|
+
* A navigation to another page under the kept screen, such as a step in a stack inside it, finds
|
|
691
|
+
* the url that screen was showing there instead. The page's own entry stays, and the entries of
|
|
692
|
+
* the screens taken off between the two go.
|
|
693
|
+
*/
|
|
694
|
+
rewind({ navigation, steps, url }) {
|
|
695
|
+
const history = this.history;
|
|
696
|
+
if (!(history instanceof NativePlatformLocation))
|
|
697
|
+
return;
|
|
698
|
+
const behind = this.historyUrl(-steps);
|
|
699
|
+
if (behind !== this.router.url) {
|
|
700
|
+
if (url !== undefined && behind === url)
|
|
701
|
+
history.ɵdropBehind(steps - 1);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
history.ɵrewind(steps);
|
|
705
|
+
const state = history.getState();
|
|
706
|
+
const own = state && typeof state === 'object' ? state : {};
|
|
707
|
+
history.replaceState({ ...own, navigationId: navigation }, '', history.ɵurlAt(0));
|
|
708
|
+
}
|
|
576
709
|
/** A navigation succeeded: what it superseded goes, and what it did is no longer undoable. */
|
|
577
710
|
settle(navigation) {
|
|
711
|
+
const pending = this.router && rewinds.get(this.router);
|
|
712
|
+
if (pending?.navigation === navigation) {
|
|
713
|
+
rewinds.delete(this.router);
|
|
714
|
+
this.rewind(pending);
|
|
715
|
+
}
|
|
578
716
|
if (this.superseded?.navigation === navigation) {
|
|
579
717
|
for (const entry of this.superseded.entries)
|
|
580
718
|
this.remove(entry);
|
|
581
719
|
this.superseded = null;
|
|
582
720
|
}
|
|
721
|
+
this.dropPopped(navigation);
|
|
583
722
|
for (const entry of this.entries) {
|
|
584
723
|
if (entry.activatedIn === navigation)
|
|
585
724
|
entry.activatedIn = undefined;
|
|
@@ -595,12 +734,28 @@ export class NativeStackOutlet {
|
|
|
595
734
|
* stack inside it. That one goes, with everything in it, and the screen it covered carries on.
|
|
596
735
|
*/
|
|
597
736
|
rollBack(navigation) {
|
|
737
|
+
if (this.router && rewinds.get(this.router)?.navigation === navigation) {
|
|
738
|
+
rewinds.delete(this.router);
|
|
739
|
+
}
|
|
598
740
|
if (this.superseded?.navigation === navigation)
|
|
599
741
|
this.superseded = null;
|
|
742
|
+
this.unpop(navigation);
|
|
600
743
|
for (const entry of this.entries.filter((candidate) => candidate.activatedIn === navigation)) {
|
|
601
744
|
this.remove(entry);
|
|
602
745
|
}
|
|
603
746
|
this.uncover(navigation);
|
|
747
|
+
this.recover(navigation);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Give the router's context for this outlet back the outlet contexts of the top screen, which the
|
|
751
|
+
* navigation took when it covered that screen: the next navigation finds the outlets inside the
|
|
752
|
+
* screen through them.
|
|
753
|
+
*/
|
|
754
|
+
recover(navigation) {
|
|
755
|
+
const covered = this.top?.covered;
|
|
756
|
+
const context = this.parentContexts.getContext(this.name);
|
|
757
|
+
if (context && covered?.navigation === navigation)
|
|
758
|
+
context.children = covered.children;
|
|
604
759
|
}
|
|
605
760
|
/**
|
|
606
761
|
* Give the top screen back its change detection when the router covered it for a navigation
|
|
@@ -625,6 +780,36 @@ export class NativeStackOutlet {
|
|
|
625
780
|
this.renderer.setStyle(screen, key, value);
|
|
626
781
|
}
|
|
627
782
|
}
|
|
783
|
+
/**
|
|
784
|
+
* On iOS 26 and later, the view a page's content commits in, as react-native-screens'
|
|
785
|
+
* `ScreenStackItem` wraps it in its `SafeAreaView` there. From 4.30 the screen runs under the
|
|
786
|
+
* navigation bar whether the bar is opaque or not
|
|
787
|
+
* (`+[RNSScreenStackHeaderConfig screensExtendUnderOpaqueNavigationBar]`), and this view insets
|
|
788
|
+
* the content below an opaque one. Before 4.30 the screen starts below such a bar, its safe area
|
|
789
|
+
* is zero at the top, and the view insets by nothing. Null elsewhere: the content commits
|
|
790
|
+
* straight into the screen.
|
|
791
|
+
*/
|
|
792
|
+
insetContent(screen) {
|
|
793
|
+
if (!this.underBar)
|
|
794
|
+
return null;
|
|
795
|
+
const content = ownHost(this.renderer.createElement('screen-content'));
|
|
796
|
+
this.renderer.appendChild(screen, content);
|
|
797
|
+
screen.contentView = content;
|
|
798
|
+
return content;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The top edge while the page's header is shown and not translucent, which a large title on iOS
|
|
802
|
+
* is, and no edge otherwise, as `getSafeAreaEdges` in `ScreenStackItem` chooses. The view stays
|
|
803
|
+
* when the header changes, so the content is not made again. Native takes all four edges.
|
|
804
|
+
*/
|
|
805
|
+
followHeader(content, header, injector) {
|
|
806
|
+
effect(() => this.renderer.setProperty(content, 'edges', {
|
|
807
|
+
top: header(),
|
|
808
|
+
right: false,
|
|
809
|
+
bottom: false,
|
|
810
|
+
left: false,
|
|
811
|
+
}), { injector });
|
|
812
|
+
}
|
|
628
813
|
createScreen(presentation, dismissed = (count) => this.onNativeDismiss(count)) {
|
|
629
814
|
const screen = ownHost(this.renderer.createElement('screen'));
|
|
630
815
|
// From before the presentation is set, so a value it or the page's host bindings write counts
|
|
@@ -637,6 +822,11 @@ export class NativeStackOutlet {
|
|
|
637
822
|
// Without this the native stack and the router disagree from that moment on: the screen is
|
|
638
823
|
// gone but the URL still points at it, so the next navigation starts from the wrong place.
|
|
639
824
|
this.renderer.listen(screen, 'dismissed', (event) => dismissed(event?.nativeEvent?.dismissCount ?? 1));
|
|
825
|
+
// Android's header back arrow pops the screen natively unless this is off, and then it ignores
|
|
826
|
+
// `preventNativeDismiss`, as its Back button does. Off, it is sent here instead, and goes the
|
|
827
|
+
// way Back does: react-navigation's native stack turns it off for the same reason.
|
|
828
|
+
this.renderer.setProperty(screen, 'nativeBackButtonDismissalEnabled', false);
|
|
829
|
+
this.renderer.listen(screen, 'headerBackButtonClicked', () => this.onHeaderBack(screen));
|
|
640
830
|
// Every key is a prop `RNSScreen` already declares, spelled the way the codegen spec spells
|
|
641
831
|
// it, so this forwards rather than translates. Set before the screen joins the stack: the
|
|
642
832
|
// native side reads the presentation as it mounts.
|
|
@@ -708,6 +898,24 @@ export class NativeStackOutlet {
|
|
|
708
898
|
return false;
|
|
709
899
|
return this.refuseBack(this.top.screen);
|
|
710
900
|
}
|
|
901
|
+
/**
|
|
902
|
+
* Android's header back arrow on `screen`: refused where the screen refuses, as Back is, and
|
|
903
|
+
* otherwise a step back, or the presented page dismissed when the screen is that page's.
|
|
904
|
+
* react-native-screens sends the arrow of a stack's first screen on the screen the stack sits
|
|
905
|
+
* in, which holds the top screen's refusal for it.
|
|
906
|
+
*/
|
|
907
|
+
onHeaderBack(screen) {
|
|
908
|
+
if (screen === this.presentedPage?.screen) {
|
|
909
|
+
if (!this.refuseBack(screen))
|
|
910
|
+
this.dismissPresentedPage();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
// A tab behind keeps its top screen, but nobody can tap its arrow: like Back, it is not asked.
|
|
914
|
+
if (screen !== this.top?.screen || !this.isShowing())
|
|
915
|
+
return;
|
|
916
|
+
if (!this.refuseBack(screen))
|
|
917
|
+
this.goBack();
|
|
918
|
+
}
|
|
711
919
|
/** Refuses Android's Back for a screen that sets `preventNativeDismiss`, and tells its page. */
|
|
712
920
|
refuseBack(screen) {
|
|
713
921
|
if (!refuses(screen))
|
|
@@ -791,7 +999,7 @@ export class NativeStackOutlet {
|
|
|
791
999
|
this.location.historyGo(-steps);
|
|
792
1000
|
return;
|
|
793
1001
|
}
|
|
794
|
-
void this.
|
|
1002
|
+
void this.navigateDownTo(below);
|
|
795
1003
|
}
|
|
796
1004
|
/**
|
|
797
1005
|
* Take the router down to the screen at `index`, popping every screen above it together.
|
|
@@ -817,7 +1025,20 @@ export class NativeStackOutlet {
|
|
|
817
1025
|
this.location.historyGo(-steps);
|
|
818
1026
|
return arrived;
|
|
819
1027
|
}
|
|
820
|
-
return this.
|
|
1028
|
+
return this.navigateDownTo(this.entries[index]);
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Send the router to `entry` by a navigation that replaces the entry, for a pop the history
|
|
1032
|
+
* behind cannot make. With the same url on the stack twice, the reuse strategy is told which of
|
|
1033
|
+
* the screens at it this is.
|
|
1034
|
+
*/
|
|
1035
|
+
navigateDownTo(entry) {
|
|
1036
|
+
const router = this.router;
|
|
1037
|
+
poppingTo.set(router, entry.ref);
|
|
1038
|
+
return router.navigateByUrl(this.urlOf(entry), { replaceUrl: true }).finally(() => {
|
|
1039
|
+
if (poppingTo.get(router) === entry.ref)
|
|
1040
|
+
poppingTo.delete(router);
|
|
1041
|
+
});
|
|
821
1042
|
}
|
|
822
1043
|
/** Whether the navigation that happens next succeeds. */
|
|
823
1044
|
nextNavigation() {
|
|
@@ -834,6 +1055,11 @@ export class NativeStackOutlet {
|
|
|
834
1055
|
});
|
|
835
1056
|
});
|
|
836
1057
|
}
|
|
1058
|
+
/** The url the app showed before the navigation in flight, as the router writes it. */
|
|
1059
|
+
previousUrl() {
|
|
1060
|
+
const previous = this.router?.getCurrentNavigation()?.previousNavigation?.finalUrl;
|
|
1061
|
+
return previous && this.router.serializeUrl(previous);
|
|
1062
|
+
}
|
|
837
1063
|
/** The url a screen was showing when it was covered, rebuilt from its own route snapshot. */
|
|
838
1064
|
urlOf(entry) {
|
|
839
1065
|
const snapshot = entry.route.snapshot;
|
|
@@ -859,7 +1085,7 @@ export class NativeStackOutlet {
|
|
|
859
1085
|
* register in, and whether its screen is in front - which is this screen's own say and, for a stack inside a
|
|
860
1086
|
* screen of another, that screen's too.
|
|
861
1087
|
*/
|
|
862
|
-
outletInjector(route, inFront, screen, outlet = this.name) {
|
|
1088
|
+
outletInjector(route, inFront, screen, header, outlet = this.name) {
|
|
863
1089
|
const outer = this.injector.get(SCREEN_IN_FRONT);
|
|
864
1090
|
return Injector.create({
|
|
865
1091
|
parent: this.injector,
|
|
@@ -873,7 +1099,7 @@ export class NativeStackOutlet {
|
|
|
873
1099
|
},
|
|
874
1100
|
},
|
|
875
1101
|
{ provide: ActivatedRoute, useValue: route },
|
|
876
|
-
{ provide: SCREEN_HEADER, useValue:
|
|
1102
|
+
{ provide: SCREEN_HEADER, useValue: header },
|
|
877
1103
|
{
|
|
878
1104
|
provide: ChildrenOutletContexts,
|
|
879
1105
|
useValue: this.parentContexts.getOrCreateContext(outlet).children,
|