@ng-native/router 0.0.1 → 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -1
  3. package/dist/bind-route-inputs.d.ts +20 -0
  4. package/dist/bind-route-inputs.js +52 -0
  5. package/dist/bind-route-inputs.js.map +1 -0
  6. package/dist/full-window-overlay.d.ts +16 -0
  7. package/dist/full-window-overlay.js +52 -0
  8. package/dist/full-window-overlay.js.map +1 -0
  9. package/dist/index.d.ts +26 -0
  10. package/dist/index.js +27 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/native-back.d.ts +38 -0
  13. package/dist/native-back.js +88 -0
  14. package/dist/native-back.js.map +1 -0
  15. package/dist/native-bar-defaults.d.ts +50 -0
  16. package/dist/native-bar-defaults.js +17 -0
  17. package/dist/native-bar-defaults.js.map +1 -0
  18. package/dist/native-header-item.d.ts +12 -0
  19. package/dist/native-header-item.js +40 -0
  20. package/dist/native-header-item.js.map +1 -0
  21. package/dist/native-header-palette.d.ts +45 -0
  22. package/dist/native-header-palette.js +42 -0
  23. package/dist/native-header-palette.js.map +1 -0
  24. package/dist/native-header.d.ts +103 -0
  25. package/dist/native-header.js +204 -0
  26. package/dist/native-header.js.map +1 -0
  27. package/dist/native-links.d.ts +30 -0
  28. package/dist/native-links.js +40 -0
  29. package/dist/native-links.js.map +1 -0
  30. package/dist/native-navigation.d.ts +84 -0
  31. package/dist/native-navigation.js +153 -0
  32. package/dist/native-navigation.js.map +1 -0
  33. package/dist/native-platform-location.d.ts +62 -0
  34. package/dist/native-platform-location.js +110 -0
  35. package/dist/native-platform-location.js.map +1 -0
  36. package/dist/native-router-link.d.ts +14 -0
  37. package/dist/native-router-link.js +51 -0
  38. package/dist/native-router-link.js.map +1 -0
  39. package/dist/native-search-bar.d.ts +44 -0
  40. package/dist/native-search-bar.js +132 -0
  41. package/dist/native-search-bar.js.map +1 -0
  42. package/dist/native-stack-outlet.d.ts +195 -0
  43. package/dist/native-stack-outlet.js +564 -0
  44. package/dist/native-stack-outlet.js.map +1 -0
  45. package/dist/native-stack-reuse-strategy.d.ts +57 -0
  46. package/dist/native-stack-reuse-strategy.js +126 -0
  47. package/dist/native-stack-reuse-strategy.js.map +1 -0
  48. package/dist/native-tab.d.ts +147 -0
  49. package/dist/native-tab.js +209 -0
  50. package/dist/native-tab.js.map +1 -0
  51. package/dist/native-tabs-outlet.d.ts +138 -0
  52. package/dist/native-tabs-outlet.js +408 -0
  53. package/dist/native-tabs-outlet.js.map +1 -0
  54. package/dist/own-host.d.ts +6 -0
  55. package/dist/own-host.js +28 -0
  56. package/dist/own-host.js.map +1 -0
  57. package/dist/provide-native-router.d.ts +41 -0
  58. package/dist/provide-native-router.js +151 -0
  59. package/dist/provide-native-router.js.map +1 -0
  60. package/dist/screen-presentation.d.ts +69 -0
  61. package/dist/screen-presentation.js +10 -0
  62. package/dist/screen-presentation.js.map +1 -0
  63. package/dist/screens.d.ts +37 -0
  64. package/dist/screens.js +108 -0
  65. package/dist/screens.js.map +1 -0
  66. package/dist/tab-routes.d.ts +16 -0
  67. package/dist/tab-routes.js +24 -0
  68. package/dist/tab-routes.js.map +1 -0
  69. package/dist/tab-safe-area-view.d.ts +15 -0
  70. package/dist/tab-safe-area-view.js +63 -0
  71. package/dist/tab-safe-area-view.js.map +1 -0
  72. package/dist/transforms.d.ts +2 -0
  73. package/dist/transforms.js +13 -0
  74. package/dist/transforms.js.map +1 -0
  75. package/package.json +59 -3
@@ -0,0 +1,564 @@
1
+ /**
2
+ * A `RouterOutlet` that renders a native stack instead of swapping one view for another.
3
+ *
4
+ * The outlet's host element *is* the `RNSScreenStack`, and each activated route's component is
5
+ * created directly on its own `screen` child rather than on a wrapper view inside it (ADR 0004).
6
+ * The screens below the top stay mounted, which is what keeps a pushed-away screen's native
7
+ * state alive: its scroll offset, its text cursor, its keyboard focus.
8
+ *
9
+ * Push and pop are inferred from the calls the router already makes. With a reuse strategy that
10
+ * detaches, navigating forward is `detach()` then `activateWith()`, and navigating back is
11
+ * `detach()` then `attach(ref)` with a ref we handed out earlier. Finding that ref in our own
12
+ * stack is what identifies a pop, so nothing here needs to consult the URL.
13
+ *
14
+ * Note `activityState` is set once, to active, and never lowered. `Screen.tsx` throws on any
15
+ * decrease while `isNativeStack`, because a native stack decides what is visible from the order
16
+ * of its children rather than from a per-screen flag. Toggling that flag is for `ScreenContainer`
17
+ * (tabs), where every screen is mounted at once.
18
+ */
19
+ import { ApplicationRef, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, ErrorHandler, HostAttributeToken, Injector, Renderer2, computed, createComponent, inject, signal, } from '@angular/core';
20
+ import { ActivatedRoute, ChildrenOutletContexts, NavigationCancel, NavigationEnd, NavigationError, PRIMARY_OUTLET, Router, createUrlTreeFromSnapshot, } from '@angular/router';
21
+ import { Location, PlatformLocation } from '@angular/common';
22
+ import { SCREEN_IN_FRONT } from '@ng-native/device';
23
+ import { bindRouteInputs } from "./bind-route-inputs.js";
24
+ import { NativeBack, isShowing } from "./native-back.js";
25
+ import { intentOf } from "./native-navigation.js";
26
+ import { NativePlatformLocation } from "./native-platform-location.js";
27
+ import { markScreenRoute } from "./tab-routes.js";
28
+ import { ownHost } from "./own-host.js";
29
+ import { ActivityState } from "./screens.js";
30
+ import * as i0 from "@angular/core";
31
+ /**
32
+ * The last navigation, per router, whose presentation an outlet has already given a screen.
33
+ *
34
+ * Presenting a route that is a stack of its own activates two outlets in one navigation: the one
35
+ * the route is presented in, and the one inside it that shows the route's first child. Only the
36
+ * first is presented; the second is the first screen of a stack inside a presented screen, and a
37
+ * push inside that stack must see it as a pushed screen, not one to present over.
38
+ */
39
+ const presented = new WeakMap();
40
+ /** What every screen needs to fill the stack, the same five keys RNSScreen itself expects. */
41
+ const FILL = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 };
42
+ export class NativeStackOutlet {
43
+ /**
44
+ * Named outlets work exactly as on the web: `<native-stack-outlet name="list" />` is filled by
45
+ * routes with `outlet: 'list'`. The default is the primary one. Read once, as the router's own
46
+ * outlet reads it, since an outlet cannot change which contexts it is registered under.
47
+ */
48
+ name = inject(new HostAttributeToken('name'), { optional: true }) ?? PRIMARY_OUTLET;
49
+ supportsBindingToComponentInputs = true;
50
+ parentContexts = inject(ChildrenOutletContexts);
51
+ renderer = inject(Renderer2);
52
+ applicationRef = inject(ApplicationRef);
53
+ injector = inject(Injector);
54
+ host = inject(ElementRef);
55
+ location = inject(Location);
56
+ history = inject(PlatformLocation);
57
+ /**
58
+ * Only for the intent a navigation carries. Optional so the outlet can be exercised through
59
+ * `RouterOutletContract` directly, which is how its own tests drive it.
60
+ */
61
+ router = inject(Router, { optional: true });
62
+ errors = inject(ErrorHandler);
63
+ entries = [];
64
+ /**
65
+ * The screens a replace or a reset supersedes, kept until the navigation that supersedes them
66
+ * succeeds: one that fails leaves the stack as it was.
67
+ */
68
+ superseded = null;
69
+ fill = { flex: 1 };
70
+ constructor() {
71
+ ownHost(this.host.nativeElement);
72
+ // Ours, not a prop of the native stack.
73
+ delete this.host.nativeElement.props['name'];
74
+ this.parentContexts.onChildOutletCreated(this.name, this);
75
+ // Android's back button, and `NativeNavigation.back()`. Declining at the bottom of the stack
76
+ // is deliberate: the outlet this one sits in gets its turn, and when none takes it the
77
+ // platform backgrounds the app, which is what a user expects, rather than the app swallowing
78
+ // the press and appearing stuck.
79
+ //
80
+ // Not yet verified on a device: whether react-native-screens' own Android back handling
81
+ // (`nativeBackButtonDismissalEnabled`) also pops the screen, which would make one press pop
82
+ // twice. If a back press ever skips a screen on Android, look here first.
83
+ const back = inject(NativeBack);
84
+ const unsubscribeBack = back.handle(() => this.goBack());
85
+ const unsubscribeStack = back.addStack({
86
+ showing: () => this.isShowing(),
87
+ popToRoot: () => (this.entries.length > 1 ? this.popToEntry(0) : null),
88
+ popTo: (url) => {
89
+ const index = this.entries.findIndex((entry) => this.urlOf(entry) === url);
90
+ return index !== -1 && index < this.entries.length - 1 ? this.popToEntry(index) : null;
91
+ },
92
+ });
93
+ // Optional for the same reason as the router: the outlet's own tests stand one in without it.
94
+ const navigations = this.router?.events?.subscribe((event) => {
95
+ if (event instanceof NavigationEnd)
96
+ this.settle(event.id);
97
+ else if (event instanceof NavigationError)
98
+ this.rollBack(event.id);
99
+ });
100
+ inject(DestroyRef).onDestroy(() => {
101
+ unsubscribeBack();
102
+ unsubscribeStack();
103
+ navigations?.unsubscribe();
104
+ for (const entry of this.entries) {
105
+ entry.unbind();
106
+ entry.ref.destroy();
107
+ }
108
+ this.entries.length = 0;
109
+ this.parentContexts.onChildOutletDestroyed(this.name);
110
+ });
111
+ }
112
+ /** The screens currently in the stack, oldest first. Exposed for tests. */
113
+ get depth() {
114
+ return this.entries.length;
115
+ }
116
+ // --- RouterOutletContract ----------------------------------------------
117
+ get isActivated() {
118
+ return this.top !== undefined && !this.top.detached;
119
+ }
120
+ get component() {
121
+ return this.top && !this.top.detached ? this.top.ref.instance : null;
122
+ }
123
+ get activatedRoute() {
124
+ return this.top && !this.top.detached ? this.top.route : null;
125
+ }
126
+ get activatedRouteData() {
127
+ return this.activatedRoute?.snapshot.data ?? {};
128
+ }
129
+ /** The input binder reads this to bind route params to component inputs. */
130
+ get activatedComponentRef() {
131
+ return this.top?.ref ?? null;
132
+ }
133
+ activateWith(route, environmentInjector) {
134
+ const component = (route.component ?? route.snapshot.component);
135
+ if (!component) {
136
+ throw new Error('[angular-native] route has no component to activate');
137
+ }
138
+ // Whatever a stack shows is a screen, children or not, and must survive being covered.
139
+ if (route.snapshot.routeConfig)
140
+ markScreenRoute(route.snapshot.routeConfig);
141
+ // What kind of navigation this is, over and above which route it lands on: a replace
142
+ // supersedes the screen below rather than stacking on it, and a reset empties the stack.
143
+ // Everything else leaves the screens below mounted, so their native state survives the trip.
144
+ const intent = this.currentIntent();
145
+ const navigation = this.navigationId();
146
+ const presentation = this.presentationFor(intent);
147
+ //
148
+ // The screen is the page's host element, so its props are set before the component exists
149
+ // and the component's own host bindings land on top of them. Style bindings merge key by
150
+ // key, so a page styling itself keeps the absolute fill the stack lays out with - except for
151
+ // `position` and the four insets, which a page naming them writes over outright rather than
152
+ // merging into. Those five are re-asserted below, after the page's own change detection has
153
+ // run, so the outlet's positioning always wins whatever a page's host style says.
154
+ const screen = this.createScreen(presentation);
155
+ const inFront = signal(true, /* @ts-ignore */
156
+ ...(ngDevMode ? [{ debugName: "inFront" }] : /* istanbul ignore next */ []));
157
+ const { ref, unbind } = this.createPage(component, route, environmentInjector, screen, inFront);
158
+ // The page's own host style, including any `position` or inset it declared, has now landed
159
+ // on the screen: put the stack's fill back over it. Per-key, through `setStyle`, so whatever
160
+ // else the page's style set - a background colour, say - still merges and stays.
161
+ this.fillScreen(screen);
162
+ if (intent?.stack === 'reset')
163
+ this.supersede(this.entries.slice(), navigation);
164
+ else if (intent?.stack === 'replace' && this.top)
165
+ this.supersede([this.top], navigation);
166
+ this.entries.push({
167
+ route,
168
+ ref,
169
+ screen,
170
+ detached: false,
171
+ unbind,
172
+ presentation,
173
+ activatedIn: navigation,
174
+ inFront,
175
+ });
176
+ }
177
+ /**
178
+ * The page's component, created on its screen and rendered once.
179
+ *
180
+ * A page that throws on that first render fails the navigation, and nothing of it may stay: a
181
+ * half-made screen left on the stack is one the router does not know about, with a component
182
+ * still running on it. So its component is destroyed, its screen taken off the stack, the
183
+ * screen it was pushed from given back its change detection, and the error handed to the
184
+ * app's `ErrorHandler` before the router fails the navigation with it.
185
+ */
186
+ createPage(component, route, environmentInjector, screen, inFront) {
187
+ let ref;
188
+ let unbind;
189
+ try {
190
+ ref = createComponent(component, {
191
+ environmentInjector,
192
+ elementInjector: this.outletInjector(route, inFront),
193
+ hostElement: screen,
194
+ });
195
+ this.applicationRef.attachView(ref.hostView);
196
+ // Route params onto inputs before the first change detection, not after: a required input
197
+ // read in the template throws NG0950 if it is still unset when the page first renders.
198
+ unbind = this.bindInputs(ref, route);
199
+ // Before anything can commit. Creating the page's nodes dirties the tree, which schedules a
200
+ // commit of its own - and that commit would capture the page with its elements created and
201
+ // its bindings not yet applied. On iOS the props catch up a frame later and nobody sees it;
202
+ // on Android the flattening decision is made from the props a view is created with, so a
203
+ // content wrapper that arrives without its `collapsable: false` is flattened for good and
204
+ // its children end up parented to a scroll view that allows exactly one.
205
+ ref.changeDetectorRef.detectChanges();
206
+ return { ref, unbind };
207
+ }
208
+ catch (error) {
209
+ unbind?.();
210
+ ref?.destroy();
211
+ this.removeScreen(screen);
212
+ this.uncover();
213
+ this.errors.handleError(error);
214
+ throw error;
215
+ }
216
+ }
217
+ deactivate() {
218
+ const entry = this.entries.pop();
219
+ if (!entry)
220
+ return;
221
+ this.drop(entry);
222
+ }
223
+ /**
224
+ * Hand the live `ComponentRef` back to the router without destroying it. The screen stays
225
+ * mounted, so the component keeps running and its native views keep their state.
226
+ */
227
+ detach() {
228
+ const entry = this.top;
229
+ if (!entry)
230
+ throw new Error('[angular-native] outlet is not activated, nothing to detach');
231
+ entry.detached = true;
232
+ entry.detachedIn = this.navigationId();
233
+ entry.inFront.set(false);
234
+ // `ChangeDetectorRef.detach`, not `ApplicationRef.detachView`. The latter calls
235
+ // `renderDetachView` and pulls the view's nodes out of the tree, which is exactly what we
236
+ // are trying to avoid: the screen would vanish from the stack the moment we pushed past it.
237
+ // This stops change detection for an off-screen screen while leaving it mounted.
238
+ entry.ref.hostView.detach();
239
+ return entry.ref;
240
+ }
241
+ /**
242
+ * Re-attach a previously detached ref. If we still hold it, everything stacked above it is a
243
+ * screen the user has navigated back past, so those are destroyed: that is the pop.
244
+ */
245
+ attach(ref, route) {
246
+ const index = this.entries.findIndex((entry) => entry.ref === ref);
247
+ if (index === -1) {
248
+ // Not ours: the router is restoring a ref detached by a different outlet instance. Its
249
+ // host element is already a screen, complete with the `dismissed` listener that outlet
250
+ // registered, so it is re-parented rather than rebuilt.
251
+ const screen = ref.location.nativeElement;
252
+ this.renderer.appendChild(this.host.nativeElement, screen);
253
+ this.entries.push({
254
+ route,
255
+ ref,
256
+ screen,
257
+ detached: false,
258
+ unbind: this.bindInputs(ref, route),
259
+ // What it was given is its first outlet's; this one has no say over that screen's.
260
+ inFront: signal(true),
261
+ });
262
+ this.applicationRef.attachView(ref.hostView);
263
+ }
264
+ else {
265
+ for (const above of this.entries.splice(index + 1))
266
+ this.drop(above);
267
+ const entry = this.entries[index];
268
+ entry.detached = false;
269
+ entry.inFront.set(true);
270
+ entry.route = route;
271
+ // The route object is new even when the url is not, so the inputs follow the new one.
272
+ entry.unbind();
273
+ entry.unbind = this.bindInputs(ref, route);
274
+ }
275
+ // Resume change detection for the screen the user is coming back to.
276
+ ref.hostView.reattach();
277
+ }
278
+ // --- native stack -------------------------------------------------------
279
+ /**
280
+ * Route params onto the page's inputs, but only in an app that asked for it with
281
+ * `withComponentInputBinding()`, which is the only time Angular's own outlet binds them. Read
282
+ * per screen rather than once, for a test that stands the router in.
283
+ */
284
+ bindInputs(ref, route) {
285
+ return this.router?.componentInputBindingEnabled ? bindRouteInputs(ref, route) : () => { };
286
+ }
287
+ get top() {
288
+ return this.entries[this.entries.length - 1];
289
+ }
290
+ /**
291
+ * What the navigation asked for, if it asked for anything. Read here rather than remembered,
292
+ * because the router is mid-navigation for exactly as long as an activation takes.
293
+ */
294
+ currentIntent() {
295
+ return intentOf(this.router?.getCurrentNavigation()?.extras.state);
296
+ }
297
+ /**
298
+ * How the screen a navigation activates is put on screen.
299
+ *
300
+ * A push on top of a presented screen is presented the same way. `RNSScreenStack` pushes a push
301
+ * screen that follows a modal onto the navigation controller under the modal, so the router
302
+ * would be on the new route while the modal stayed in front, and the modal's own way out would
303
+ * first pop a screen nobody could see. A presentation the navigation names itself wins, and a
304
+ * reset starts a stack with nothing to follow. A presented screen that is a stack of its own is
305
+ * where a push to one of its children goes instead, inside it.
306
+ */
307
+ presentationFor(intent) {
308
+ const own = this.claimPresentation(intent);
309
+ const covering = this.top?.presentation?.stackPresentation;
310
+ if (intent?.stack === 'reset' || !covering || covering === 'push' || own?.stackPresentation) {
311
+ return own;
312
+ }
313
+ return { ...own, stackPresentation: covering };
314
+ }
315
+ /** The navigation's own presentation, unless an outlet further out already presented it. */
316
+ claimPresentation(intent) {
317
+ if (!intent?.presentation)
318
+ return undefined;
319
+ const id = this.router?.getCurrentNavigation()?.id;
320
+ if (id === undefined)
321
+ return intent.presentation;
322
+ if (presented.get(this.router) === id)
323
+ return undefined;
324
+ presented.set(this.router, id);
325
+ return intent.presentation;
326
+ }
327
+ /** Take a screen out for good: its bindings, its component, and its native view. */
328
+ drop(entry) {
329
+ entry.unbind();
330
+ entry.ref.destroy();
331
+ this.removeScreen(entry.screen);
332
+ }
333
+ /** Take one entry out of the stack, wherever it is, and drop it. */
334
+ remove(entry) {
335
+ const index = this.entries.indexOf(entry);
336
+ if (index === -1)
337
+ return;
338
+ this.entries.splice(index, 1);
339
+ this.drop(entry);
340
+ }
341
+ // --- a navigation's outcome ----------------------------------------------
342
+ /** The id of the navigation in progress, when there is a router to say. */
343
+ navigationId() {
344
+ return this.router?.getCurrentNavigation()?.id;
345
+ }
346
+ /**
347
+ * Take out the screens a replace or a reset supersedes once its navigation has succeeded, or
348
+ * at once when there is no navigation to wait for. A replaced screen has already been detached
349
+ * by the router, and with `replaceUrl` its history entry is gone too, so nothing will attach it
350
+ * again; the reuse strategy notices the ref is destroyed and drops its handle.
351
+ */
352
+ supersede(entries, navigation) {
353
+ if (navigation === undefined) {
354
+ for (const entry of entries)
355
+ this.remove(entry);
356
+ return;
357
+ }
358
+ this.superseded = { entries, navigation };
359
+ }
360
+ /** A navigation succeeded: what it superseded goes, and what it did is no longer undoable. */
361
+ settle(navigation) {
362
+ if (this.superseded?.navigation === navigation) {
363
+ for (const entry of this.superseded.entries)
364
+ this.remove(entry);
365
+ this.superseded = null;
366
+ }
367
+ for (const entry of this.entries) {
368
+ if (entry.activatedIn === navigation)
369
+ entry.activatedIn = undefined;
370
+ if (entry.detachedIn === navigation)
371
+ entry.detachedIn = undefined;
372
+ }
373
+ }
374
+ /**
375
+ * A navigation failed, so the router is back where it started: undo what it did here too.
376
+ *
377
+ * The screen that threw has already gone (`createPage`), but a screen this outlet activated for
378
+ * the same navigation has not when the one that threw is further in, the first screen of a
379
+ * stack inside it. That one goes, with everything in it, and the screen it covered carries on.
380
+ */
381
+ rollBack(navigation) {
382
+ if (this.superseded?.navigation === navigation)
383
+ this.superseded = null;
384
+ for (const entry of this.entries.filter((candidate) => candidate.activatedIn === navigation)) {
385
+ this.remove(entry);
386
+ }
387
+ this.uncover(navigation);
388
+ }
389
+ /**
390
+ * Give the top screen back its change detection when the router covered it for a navigation
391
+ * that is not going to happen: the one given, or the one in progress.
392
+ */
393
+ uncover(navigation = this.navigationId()) {
394
+ const top = this.top;
395
+ if (!top?.detached || top.detachedIn !== navigation)
396
+ return;
397
+ top.detached = false;
398
+ top.detachedIn = undefined;
399
+ top.inFront.set(true);
400
+ top.ref.hostView.reattach();
401
+ }
402
+ /**
403
+ * `position: absolute` and the four insets, through `setStyle` rather than one `setProperty`
404
+ * so each key merges into whatever style is already on the screen instead of replacing it -
405
+ * the same merge a page's own host style gets, just asserted last so the stack's layout wins.
406
+ */
407
+ fillScreen(screen) {
408
+ for (const [key, value] of Object.entries(FILL)) {
409
+ this.renderer.setStyle(screen, key, value);
410
+ }
411
+ }
412
+ createScreen(presentation) {
413
+ const screen = ownHost(this.renderer.createElement('screen'));
414
+ this.fillScreen(screen);
415
+ // Set once and never lowered: see the note at the top of this file.
416
+ this.renderer.setProperty(screen, 'activityState', ActivityState.Active);
417
+ // A swipe-back or an Android back button pops the screen natively and only then tells JS.
418
+ // Without this the native stack and the router disagree from that moment on: the screen is
419
+ // gone but the URL still points at it, so the next navigation starts from the wrong place.
420
+ this.renderer.listen(screen, 'dismissed', (event) => this.onNativeDismiss(event?.nativeEvent?.dismissCount ?? 1));
421
+ // Every key is a prop `RNSScreen` already declares, spelled the way the codegen spec spells
422
+ // it, so this forwards rather than translates. Set before the screen joins the stack: the
423
+ // native side reads the presentation as it mounts.
424
+ for (const [prop, value] of Object.entries(presentation ?? {})) {
425
+ if (value !== undefined)
426
+ this.renderer.setProperty(screen, prop, value);
427
+ }
428
+ this.renderer.appendChild(this.host.nativeElement, screen);
429
+ return screen;
430
+ }
431
+ /**
432
+ * Follow a pop the user already made. `historyGo`, not `back()`, because iOS coalesces a long
433
+ * swipe into a single event with a count.
434
+ *
435
+ * Only ever fires for a native dismissal: verified on device that a pop this outlet drives
436
+ * itself emits `willAppear`/`appear` for the screen below but no `dismissed`, so there is no
437
+ * double-pop to guard against.
438
+ *
439
+ * The gesture itself is unverified by tooling: idb cannot synthesise a `UIScreenEdgePan` that
440
+ * UIKit accepts, so this path was reasoned from the events rather than driven end to end.
441
+ */
442
+ onNativeDismiss(count) {
443
+ this.popBy(Math.max(1, count));
444
+ }
445
+ /**
446
+ * A back, from the button or `NativeNavigation`: pop the top screen, as long as there is one
447
+ * below it and this stack is the one the user is looking at. A stack in a tab behind, or under
448
+ * a screen pushed over the tab bar, is still subscribed, and the newest one would otherwise
449
+ * answer for the stack in front.
450
+ */
451
+ goBack() {
452
+ if (this.entries.length < 2 || !this.isShowing())
453
+ return false;
454
+ this.popBy(1);
455
+ return true;
456
+ }
457
+ /** Whether the top screen is part of what the router shows now. Assumed without a router. */
458
+ isShowing() {
459
+ if (!this.router)
460
+ return true;
461
+ return this.isActivated && isShowing(this.router, this.top.route);
462
+ }
463
+ /**
464
+ * Take the router to the screen `steps` below the top, whether native already popped it or it
465
+ * is about to be popped because the router arrives there.
466
+ */
467
+ popBy(steps) {
468
+ const below = this.entries[this.entries.length - 1 - steps];
469
+ const url = below && this.router ? this.urlOf(below) : null;
470
+ // Usually the entry that far back is the screen the pop revealed, and going back through
471
+ // history keeps it tidy. Not always: a stack inside a tab shares history with the other tabs,
472
+ // so after a trip to one and back the entry behind the top screen is that other tab, and
473
+ // going back would switch tab rather than pop. Then the router is sent to the revealed
474
+ // screen instead, replacing the entry of the one that is gone.
475
+ const behind = this.historyUrl(-steps);
476
+ if (!url || behind === undefined || behind === url) {
477
+ this.location.historyGo(-steps);
478
+ return;
479
+ }
480
+ void this.router.navigateByUrl(url, { replaceUrl: true });
481
+ }
482
+ /**
483
+ * Take the router down to the screen at `index`, popping every screen above it together.
484
+ * Through history when the entries behind are these screens, so going back from there keeps
485
+ * working one screen at a time; otherwise by a navigation to it that replaces the entry, as
486
+ * `popBy` does when a tab's history interleaves.
487
+ */
488
+ popToEntry(index) {
489
+ const url = this.urlOf(this.entries[index]);
490
+ const steps = this.entries.length - 1 - index;
491
+ if (this.historyUrl(-steps) === url) {
492
+ const arrived = this.nextNavigation();
493
+ this.location.historyGo(-steps);
494
+ return arrived;
495
+ }
496
+ return this.router.navigateByUrl(url, { replaceUrl: true });
497
+ }
498
+ /** Whether the navigation that happens next succeeds. */
499
+ nextNavigation() {
500
+ return new Promise((resolve) => {
501
+ const events = this.router.events.subscribe((event) => {
502
+ if (event instanceof NavigationEnd)
503
+ resolve(true);
504
+ else if (event instanceof NavigationError || event instanceof NavigationCancel) {
505
+ resolve(false);
506
+ }
507
+ else
508
+ return;
509
+ events.unsubscribe();
510
+ });
511
+ });
512
+ }
513
+ /** The url a screen was showing when it was covered, rebuilt from its own route snapshot. */
514
+ urlOf(entry) {
515
+ const snapshot = entry.route.snapshot;
516
+ const tree = createUrlTreeFromSnapshot(snapshot, [], snapshot.queryParams, snapshot.fragment);
517
+ return this.router.serializeUrl(tree);
518
+ }
519
+ /**
520
+ * The url of the history entry `offset` away, normalised the way the router writes it: null past
521
+ * the start of history, and undefined when the history is not ours to read, so the pop has to go
522
+ * through it blind.
523
+ */
524
+ historyUrl(offset) {
525
+ if (!(this.history instanceof NativePlatformLocation))
526
+ return undefined;
527
+ const url = this.history.ɵurlAt(offset);
528
+ return url === null ? null : this.router.serializeUrl(this.router.parseUrl(url));
529
+ }
530
+ removeScreen(screen) {
531
+ this.renderer.removeChild(this.host.nativeElement, screen);
532
+ }
533
+ /**
534
+ * What a page is created with: its route, the outlet contexts its own outlets register in, and
535
+ * whether its screen is in front - which is this screen's own say and, for a stack inside a
536
+ * screen of another, that screen's too.
537
+ */
538
+ outletInjector(route, inFront) {
539
+ const outer = this.injector.get(SCREEN_IN_FRONT);
540
+ return Injector.create({
541
+ parent: this.injector,
542
+ providers: [
543
+ { provide: SCREEN_IN_FRONT, useValue: computed(() => outer() && inFront()) },
544
+ { provide: ActivatedRoute, useValue: route },
545
+ {
546
+ provide: ChildrenOutletContexts,
547
+ useValue: this.parentContexts.getOrCreateContext(this.name).children,
548
+ },
549
+ ],
550
+ });
551
+ }
552
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: NativeStackOutlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
553
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.2.0", type: NativeStackOutlet, isStandalone: true, selector: "native-stack-outlet", host: { properties: { "style": "fill" } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
554
+ }
555
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: NativeStackOutlet, decorators: [{
556
+ type: Component,
557
+ args: [{
558
+ selector: 'native-stack-outlet',
559
+ template: '',
560
+ changeDetection: ChangeDetectionStrategy.OnPush,
561
+ host: { '[style]': 'fill' },
562
+ }]
563
+ }], ctorParameters: () => [] });
564
+ //# sourceMappingURL=native-stack-outlet.js.map