@ng-native/router 0.17.1 → 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.
Files changed (54) hide show
  1. package/dist/discard-failed-entries.d.ts +11 -0
  2. package/dist/discard-failed-entries.js +48 -0
  3. package/dist/discard-failed-entries.js.map +1 -0
  4. package/dist/in-tab.d.ts +9 -1
  5. package/dist/in-tab.js +2 -0
  6. package/dist/in-tab.js.map +1 -1
  7. package/dist/native-back.d.ts +21 -1
  8. package/dist/native-back.js +94 -3
  9. package/dist/native-back.js.map +1 -1
  10. package/dist/native-header.d.ts +8 -1
  11. package/dist/native-header.js +18 -4
  12. package/dist/native-header.js.map +1 -1
  13. package/dist/native-navigation.d.ts +10 -2
  14. package/dist/native-navigation.js +45 -9
  15. package/dist/native-navigation.js.map +1 -1
  16. package/dist/native-platform-location.d.ts +42 -0
  17. package/dist/native-platform-location.js +66 -0
  18. package/dist/native-platform-location.js.map +1 -1
  19. package/dist/native-stack-outlet.d.ts +83 -0
  20. package/dist/native-stack-outlet.js +245 -19
  21. package/dist/native-stack-outlet.js.map +1 -1
  22. package/dist/native-stack-reuse-strategy.d.ts +34 -0
  23. package/dist/native-stack-reuse-strategy.js +119 -7
  24. package/dist/native-stack-reuse-strategy.js.map +1 -1
  25. package/dist/native-tab.d.ts +8 -6
  26. package/dist/native-tab.js +23 -2
  27. package/dist/native-tab.js.map +1 -1
  28. package/dist/native-tabs-outlet.d.ts +9 -3
  29. package/dist/native-tabs-outlet.js +21 -4
  30. package/dist/native-tabs-outlet.js.map +1 -1
  31. package/dist/popping-to.d.ts +8 -0
  32. package/dist/popping-to.js +2 -0
  33. package/dist/popping-to.js.map +1 -0
  34. package/dist/presented-route.js +12 -10
  35. package/dist/presented-route.js.map +1 -1
  36. package/dist/provide-native-router.js +4 -0
  37. package/dist/provide-native-router.js.map +1 -1
  38. package/dist/screen-header.d.ts +5 -3
  39. package/dist/screen-header.js +5 -3
  40. package/dist/screen-header.js.map +1 -1
  41. package/dist/screen-presentation.d.ts +5 -0
  42. package/dist/screen-presentation.js.map +1 -1
  43. package/dist/screen-safe-area-view.d.ts +6 -1
  44. package/dist/screen-safe-area-view.js +12 -4
  45. package/dist/screen-safe-area-view.js.map +1 -1
  46. package/dist/screens-version.d.ts +12 -0
  47. package/dist/screens-version.js +20 -0
  48. package/dist/screens-version.js.map +1 -0
  49. package/dist/screens.js +3 -0
  50. package/dist/screens.js.map +1 -1
  51. package/dist/url-commands.d.ts +24 -0
  52. package/dist/url-commands.js +102 -0
  53. package/dist/url-commands.js.map +1 -0
  54. 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
- if (event instanceof NavigationEnd)
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
- for (const above of this.entries.splice(index + 1))
461
- this.drop(above);
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.router.navigateByUrl(url, { replaceUrl: true });
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.router.navigateByUrl(url, { replaceUrl: true });
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: signal(false) },
1102
+ { provide: SCREEN_HEADER, useValue: header },
877
1103
  {
878
1104
  provide: ChildrenOutletContexts,
879
1105
  useValue: this.parentContexts.getOrCreateContext(outlet).children,