@ng-native/router 0.3.0 → 0.5.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 (38) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/native-back.d.ts +24 -0
  5. package/dist/native-back.js +29 -0
  6. package/dist/native-back.js.map +1 -1
  7. package/dist/native-bar-defaults.d.ts +7 -0
  8. package/dist/native-bar-defaults.js.map +1 -1
  9. package/dist/native-header.d.ts +21 -1
  10. package/dist/native-header.js +45 -8
  11. package/dist/native-header.js.map +1 -1
  12. package/dist/native-navigation.d.ts +19 -0
  13. package/dist/native-navigation.js +81 -10
  14. package/dist/native-navigation.js.map +1 -1
  15. package/dist/native-stack-outlet.d.ts +50 -4
  16. package/dist/native-stack-outlet.js +202 -12
  17. package/dist/native-stack-outlet.js.map +1 -1
  18. package/dist/native-stack-reuse-strategy.d.ts +18 -6
  19. package/dist/native-stack-reuse-strategy.js +71 -19
  20. package/dist/native-stack-reuse-strategy.js.map +1 -1
  21. package/dist/native-tabs-outlet.d.ts +13 -0
  22. package/dist/native-tabs-outlet.js +52 -3
  23. package/dist/native-tabs-outlet.js.map +1 -1
  24. package/dist/presented-route.d.ts +27 -0
  25. package/dist/presented-route.js +184 -0
  26. package/dist/presented-route.js.map +1 -0
  27. package/dist/screen-header.d.ts +10 -0
  28. package/dist/screen-header.js +11 -0
  29. package/dist/screen-header.js.map +1 -0
  30. package/dist/screen-safe-area-view.d.ts +18 -0
  31. package/dist/screen-safe-area-view.js +74 -0
  32. package/dist/screen-safe-area-view.js.map +1 -0
  33. package/dist/screens.js +3 -2
  34. package/dist/screens.js.map +1 -1
  35. package/dist/tab-safe-area-view.d.ts +4 -11
  36. package/dist/tab-safe-area-view.js +8 -43
  37. package/dist/tab-safe-area-view.js.map +1 -1
  38. package/package.json +3 -3
@@ -16,6 +16,7 @@
16
16
  import { inject } from '@angular/core';
17
17
  import { NavigationCancel, NavigationEnd, NavigationError, Router, } from '@angular/router';
18
18
  import { NativeBack } from "./native-back.js";
19
+ import { leavingPresented, presentedCommands } from "./presented-route.js";
19
20
  /**
20
21
  * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under
21
22
  * one reserved key rather than spread through it.
@@ -61,10 +62,16 @@ export class NativeNavigation {
61
62
  */
62
63
  present(commands, options = {}) {
63
64
  const { as = 'modal', presentation, ...extras } = options;
64
- return this.go(commands, extras, {
65
+ const intent = {
65
66
  stack: 'push',
66
67
  presentation: { stackPresentation: as, ...presentation },
67
- });
68
+ };
69
+ // Asked at once, and only waited for when the page is another tab's: finding its route may
70
+ // load a lazy one.
71
+ const over = this.overTheTabInFront(commands, extras);
72
+ if (!over)
73
+ return this.go(commands, extras, intent);
74
+ return over.then((found) => this.go(found?.commands ?? commands, found?.extras ?? extras, intent));
68
75
  }
69
76
  /**
70
77
  * Replace the whole stack with one screen, the way signing out or finishing onboarding does.
@@ -79,8 +86,29 @@ export class NativeNavigation {
79
86
  * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.
80
87
  */
81
88
  back() {
82
- this.outlets.back();
89
+ // Optional call: a test that stands a plain object in for the router has no such signal.
90
+ if (!this.router.currentNavigation?.()) {
91
+ this.outlets.back();
92
+ return;
93
+ }
94
+ // Asked while a navigation is still putting its screen up: a page that leaves as it appears,
95
+ // from an effect in its constructor. No stack has that screen to pop yet, so the back waits
96
+ // for the navigation, and goes nowhere if the navigation does not arrive.
97
+ if (this.backWaiting)
98
+ return;
99
+ this.backWaiting = this.router.events.subscribe((event) => {
100
+ const ended = event instanceof NavigationEnd;
101
+ if (!ended && !(event instanceof NavigationCancel) && !(event instanceof NavigationError)) {
102
+ return;
103
+ }
104
+ this.backWaiting?.unsubscribe();
105
+ this.backWaiting = null;
106
+ if (ended)
107
+ this.outlets.back();
108
+ });
83
109
  }
110
+ /** The wait of a back asked for mid-navigation, so two asked for in one are one. */
111
+ backWaiting = null;
84
112
  /**
85
113
  * Pop straight back to the screen at `commands`, taking every screen above it off the stack at
86
114
  * once - `popToViewController`. The screen is the one already there, with its state, not a new
@@ -105,18 +133,61 @@ export class NativeNavigation {
105
133
  const full = presentation
106
134
  ? { ...intent, presentation: { ...intent.presentation, ...presentation } }
107
135
  : intent;
108
- const navigate = () => this.router.navigate(typeof commands === 'string' ? [pathOf(commands)] : [...commands], {
109
- ...extras,
110
- ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),
111
- state: { ...state, [NATIVE_INTENT]: full },
112
- });
136
+ const navigate = () => {
137
+ const list = typeof commands === 'string' ? [pathOf(commands)] : [...commands];
138
+ const all = {
139
+ ...extras,
140
+ ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),
141
+ state: { ...state, [NATIVE_INTENT]: full },
142
+ };
143
+ const leaving = leavingPresented(this.router, list, all);
144
+ return leaving ? this.router.navigateByUrl(leaving, all) : this.router.navigate(list, all);
145
+ };
113
146
  // A screen stacked on the first one waits for it. Navigating while the router's first
114
147
  // navigation is still running - a lazily loaded root waiting on its import - cancels that
115
148
  // navigation, and the app opens on this screen with nothing beneath it. A reset replaces the
116
149
  // stack anyway, so it goes at once.
150
+ const start = () => this.underTabRoot(commands, extras, full, navigate);
117
151
  return intent.stack !== 'reset' && this.firstNavigationRunning()
118
- ? this.firstNavigation().then(navigate)
119
- : navigate();
152
+ ? this.firstNavigation().then(start)
153
+ : start();
154
+ }
155
+ /**
156
+ * A push into a tab nobody has opened goes to the tab first, so its stack has the tab's own
157
+ * first screen under the page, and a back from the page lands there: what a tab bar with a
158
+ * navigation controller per tab does. Without it the page is the stack's only screen, and the
159
+ * same push leaves a different stack behind depending on whether the tab was ever tapped. A
160
+ * presented screen goes where it is asked, and a replace or a reset has nothing to keep.
161
+ */
162
+ underTabRoot(commands, extras, intent, navigate) {
163
+ if (intent.stack !== 'push' || intent.presentation)
164
+ return navigate();
165
+ const tab = this.outlets.unopenedTabOf(this.pathFor(commands, extras));
166
+ if (!tab)
167
+ return navigate();
168
+ return this.router.navigateByUrl(tab).then((arrived) => (arrived ? navigate() : false));
169
+ }
170
+ /** The path the commands lead to, without its query and fragment. */
171
+ pathFor(commands, extras) {
172
+ const url = typeof commands === 'string'
173
+ ? commands
174
+ : this.router.serializeUrl(this.router.createUrlTree([...commands], extras));
175
+ return pathOf(url);
176
+ }
177
+ /**
178
+ * A page of a tab that is not in front, presented over the one that is: its url would select
179
+ * its own tab and show it there, so it goes to the root's presented outlet instead, where the
180
+ * app's own stack shows it over everything. See `presented-route.ts`. Null for a page of the
181
+ * tab in front, or of no tab, which is presented where its url puts it.
182
+ */
183
+ overTheTabInFront(commands, extras) {
184
+ if (!this.outlets.presents)
185
+ return null;
186
+ const path = this.pathFor(commands, extras);
187
+ if (!this.outlets.inTabBehind(path))
188
+ return null;
189
+ const query = typeof commands === 'string' ? this.queryOf(commands, extras) : {};
190
+ return presentedCommands(this.router, path).then((presented) => presented && { commands: presented, extras: { ...extras, ...query, relativeTo: null } });
120
191
  }
121
192
  /**
122
193
  * The query and fragment written into a url string, as the extras `navigate` takes them: it
@@ -1 +1 @@
1
- {"version":3,"file":"native-navigation.js","sourceRoot":"","sources":["../src/native-navigation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The typed facade over `Router` for the intents a URL cannot express (ADR 0003).\n *\n * Angular's `Router` stays the single source of truth: each of these is a line over\n * `router.navigate`, setting `replaceUrl` and a presentation on `NavigationExtras.state`. Deep\n * links, guards and resolvers keep working because it is still the Router, and because state\n * rides in history a back restores the presentation the screen was opened with.\n *\n * A navigation service that owns the stack was rejected: it either re-implements guards,\n * resolvers and deep linking, or it drifts from the URL, and once the stack is not derived from\n * router state the two can disagree.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`,\n * exactly as `NativePlatformLocation` and `NativeStackReuseStrategy` are.\n */\nimport { inject } from '@angular/core';\nimport {\n NavigationCancel,\n NavigationEnd,\n NavigationError,\n Router,\n type NavigationExtras,\n} from '@angular/router';\nimport { NativeBack } from './native-back.ts';\nimport type { ScreenPresentation, StackPresentation } from './screen-presentation.ts';\n\n/** What `Router` takes: a url string, or the array form with segments and params. */\nexport type NavigationCommands = string | readonly unknown[];\n\n/**\n * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under\n * one reserved key rather than spread through it.\n */\nexport const NATIVE_INTENT = 'ɵnativeStack';\n\n/** What a navigation asks the outlet to do with the stack, over and above rendering the route. */\nexport interface NativeIntent {\n /** `replace` drops the screen it supersedes; `reset` empties the stack first. */\n readonly stack?: 'push' | 'replace' | 'reset';\n readonly presentation?: ScreenPresentation;\n}\n\nexport interface NativeNavigationOptions extends NavigationExtras {\n /** How the screen arrives: presentation, animation, gesture and sheet props. */\n presentation?: ScreenPresentation;\n}\n\nexport interface PresentOptions extends NativeNavigationOptions {\n /** Shorthand for `presentation.stackPresentation`. Defaults to `modal`. */\n as?: StackPresentation;\n}\n\n/** A url string up to its query or fragment. */\nconst pathOf = (url: string): string => url.split(/[?#]/, 1)[0]!;\n\n/** Read the intent off a navigation's state, if it carries one. */\nexport function intentOf(state: unknown): NativeIntent | null {\n if (!state || typeof state !== 'object') return null;\n const intent = (state as Record<string, unknown>)[NATIVE_INTENT];\n return intent && typeof intent === 'object' ? (intent as NativeIntent) : null;\n}\n\nexport class NativeNavigation {\n private readonly router = inject(Router);\n private readonly outlets = inject(NativeBack);\n\n /**\n * Push a new screen onto the stack. The plain case, and what `nativeRouterLink` does.\n *\n * From a presented screen the new one is presented the same way, over it, since a pushed\n * screen would slide in under it; `presentation` says otherwise, `stackAnimation` and all.\n */\n push(commands: NavigationCommands, options: NativeNavigationOptions = {}): Promise<boolean> {\n return this.go(commands, options, { stack: 'push' });\n }\n\n /**\n * Swap the current screen for another. The history entry is replaced too, so a back from the\n * new screen goes where a back from the old one would have.\n */\n replace(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'replace' });\n }\n\n /**\n * Present a screen over the stack rather than pushing into it: a modal, or a sheet.\n *\n * `present('/filters', { as: 'formSheet', presentation: { sheetAllowedDetents: [0.5, 1] } })`.\n *\n * A presented screen is put on screen outside the stack's navigation controller, so it has no\n * native header and no automatic safe-area insets: it needs its own way out, and its own\n * `safe-area-view`. A sheet can also be dragged away, but `fullScreenModal` cannot, and with\n * `gestureEnabled: false` neither can.\n */\n present(commands: NavigationCommands, options: PresentOptions = {}): Promise<boolean> {\n const { as = 'modal', presentation, ...extras } = options;\n return this.go(commands, extras, {\n stack: 'push',\n presentation: { stackPresentation: as, ...presentation },\n });\n }\n\n /**\n * Replace the whole stack with one screen, the way signing out or finishing onboarding does.\n * Every screen below is destroyed, so there is nothing to go back to.\n */\n reset(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'reset' });\n }\n\n /**\n * Go back exactly as the Android back button does: pop the stack in front, or at the root of a\n * tab's stack, go to the first tab. Not one entry back through history, which after a trip to\n * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.\n */\n back(): void {\n this.outlets.back();\n }\n\n /**\n * Pop straight back to the screen at `commands`, taking every screen above it off the stack at\n * once - `popToViewController`. The screen is the one already there, with its state, not a new\n * one. Resolves false, doing nothing, when no screen below the top of a stack in front is\n * showing that url; a push is the way to a screen that is not there.\n */\n popTo(commands: NavigationCommands): Promise<boolean> {\n const tree =\n typeof commands === 'string'\n ? this.router.parseUrl(commands)\n : this.router.createUrlTree([...commands]);\n return this.outlets.popTo(this.router.serializeUrl(tree));\n }\n\n /**\n * Pop the stack in front to its first screen - `popToRootViewController`. Inside a tab, that is\n * the tab's first screen; the screens below keep their state. Resolves false at the root already.\n */\n popToRoot(): Promise<boolean> {\n return this.outlets.popToRoot();\n }\n\n private go(\n commands: NavigationCommands,\n options: NativeNavigationOptions,\n intent: NativeIntent,\n ): Promise<boolean> {\n const { presentation, state, ...extras } = options;\n const full: NativeIntent = presentation\n ? { ...intent, presentation: { ...intent.presentation, ...presentation } }\n : intent;\n\n const navigate = () =>\n this.router.navigate(typeof commands === 'string' ? [pathOf(commands)] : [...commands], {\n ...extras,\n ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),\n state: { ...(state as object), [NATIVE_INTENT]: full },\n });\n // A screen stacked on the first one waits for it. Navigating while the router's first\n // navigation is still running - a lazily loaded root waiting on its import - cancels that\n // navigation, and the app opens on this screen with nothing beneath it. A reset replaces the\n // stack anyway, so it goes at once.\n return intent.stack !== 'reset' && this.firstNavigationRunning()\n ? this.firstNavigation().then(navigate)\n : navigate();\n }\n\n /**\n * The query and fragment written into a url string, as the extras `navigate` takes them: it\n * reads a string command as one path segment, and would encode a `?` into it. Extras given\n * alongside win over the ones in the string.\n */\n private queryOf(url: string, extras: NavigationExtras): NavigationExtras {\n const cut = url.search(/[?#]/);\n if (cut < 0) return {};\n const { queryParams, fragment } = this.router.parseUrl(url.slice(cut));\n return {\n queryParams: { ...queryParams, ...extras.queryParams },\n fragment: extras.fragment ?? fragment ?? undefined,\n };\n }\n\n private firstNavigationRunning(): boolean {\n const router = this.router as Partial<Router>;\n return router.navigated === false && !!router.currentNavigation?.();\n }\n\n private firstNavigation(): Promise<void> {\n return new Promise((resolve) => {\n const events = this.router.events.subscribe((event) => {\n if (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError\n ) {\n events.unsubscribe();\n resolve();\n }\n });\n });\n }\n}\n"]}
1
+ {"version":3,"file":"native-navigation.js","sourceRoot":"","sources":["../src/native-navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,MAAM,GAEP,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAK3E;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,CAAC;AAmB5C,gDAAgD;AAChD,MAAM,MAAM,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC;AAEjE,mEAAmE;AACnE,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,MAAM,GAAI,KAAiC,CAAC,aAAa,CAAC,CAAC;IACjE,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAuB,CAAC,CAAC,CAAC,IAAI,CAAC;AAChF,CAAC;AAED,MAAM,OAAO,gBAAgB;IACV,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACxB,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IAE9C;;;;;OAKG;IACH,IAAI,CAAC,QAA4B,EAAE,UAAmC,EAAE;QACtE,OAAO,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,QAA4B,EAAE,UAA4B,EAAE;QAClE,OAAO,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IACnF,CAAC;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,QAA4B,EAAE,UAA0B,EAAE;QAChE,MAAM,EAAE,EAAE,GAAG,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;QAC1D,MAAM,MAAM,GAAiB;YAC3B,KAAK,EAAE,MAAM;YACb,YAAY,EAAE,EAAE,iBAAiB,EAAE,EAAE,EAAE,GAAG,YAAY,EAAE;SACzD,CAAC;QACF,2FAA2F;QAC3F,mBAAmB;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CACzB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,IAAI,QAAQ,EAAE,KAAK,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,CAAC,CACtE,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAA4B,EAAE,UAA4B,EAAE;QAChE,OAAO,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACH,IAAI;QACF,yFAAyF;QACzF,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,EAAE,CAAC;YACvC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QACD,6FAA6F;QAC7F,4FAA4F;QAC5F,0EAA0E;QAC1E,IAAI,IAAI,CAAC,WAAW;YAAE,OAAO;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YACxD,MAAM,KAAK,GAAG,KAAK,YAAY,aAAa,CAAC;YAC7C,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,YAAY,gBAAgB,CAAC,IAAI,CAAC,CAAC,KAAK,YAAY,eAAe,CAAC,EAAE,CAAC;gBAC1F,OAAO;YACT,CAAC;YACD,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,CAAC;YAChC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;YACxB,IAAI,KAAK;gBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACjC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,oFAAoF;IAC5E,WAAW,GAAmC,IAAI,CAAC;IAE3D;;;;;OAKG;IACH,KAAK,CAAC,QAA4B;QAChC,MAAM,IAAI,GACR,OAAO,QAAQ,KAAK,QAAQ;YAC1B,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAChC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED;;;OAGG;IACH,SAAS;QACP,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;IAClC,CAAC;IAEO,EAAE,CACR,QAA4B,EAC5B,OAAgC,EAChC,MAAoB;QAEpB,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;QACnD,MAAM,IAAI,GAAiB,YAAY;YACrC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,YAAY,EAAE,EAAE,GAAG,MAAM,CAAC,YAAY,EAAE,GAAG,YAAY,EAAE,EAAE;YAC1E,CAAC,CAAC,MAAM,CAAC;QAEX,MAAM,QAAQ,GAAG,GAAG,EAAE;YACpB,MAAM,IAAI,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;YAC/E,MAAM,GAAG,GAAG;gBACV,GAAG,MAAM;gBACT,GAAG,CAAC,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,KAAK,EAAE,EAAE,GAAI,KAAgB,EAAE,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE;aACvD,CAAC;YACF,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;YACzD,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC7F,CAAC,CAAC;QACF,sFAAsF;QACtF,0FAA0F;QAC1F,6FAA6F;QAC7F,oCAAoC;QACpC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACxE,OAAO,MAAM,CAAC,KAAK,KAAK,OAAO,IAAI,IAAI,CAAC,sBAAsB,EAAE;YAC9D,CAAC,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;YACpC,CAAC,CAAC,KAAK,EAAE,CAAC;IACd,CAAC;IAED;;;;;;OAMG;IACK,YAAY,CAClB,QAA4B,EAC5B,MAAwB,EACxB,MAAoB,EACpB,QAAgC;QAEhC,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM,IAAI,MAAM,CAAC,YAAY;YAAE,OAAO,QAAQ,EAAE,CAAC;QACtE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,GAAG;YAAE,OAAO,QAAQ,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1F,CAAC;IAED,qEAAqE;IAC7D,OAAO,CAAC,QAA4B,EAAE,MAAwB;QACpE,MAAM,GAAG,GACP,OAAO,QAAQ,KAAK,QAAQ;YAC1B,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACjF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAED;;;;;OAKG;IACK,iBAAiB,CACvB,QAA4B,EAC5B,MAAwB;QAExB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACjD,MAAM,KAAK,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjF,OAAO,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAC9C,CAAC,SAAS,EAAE,EAAE,CACZ,SAAS,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,CAC1F,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACK,OAAO,CAAC,GAAW,EAAE,MAAwB;QACnD,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,GAAG,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QACvB,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,OAAO;YACL,WAAW,EAAE,EAAE,GAAG,WAAW,EAAE,GAAG,MAAM,CAAC,WAAW,EAAE;YACtD,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,QAAQ,IAAI,SAAS;SACnD,CAAC;IACJ,CAAC;IAEO,sBAAsB;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAyB,CAAC;QAC9C,OAAO,MAAM,CAAC,SAAS,KAAK,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,CAAC;IACtE,CAAC;IAEO,eAAe;QACrB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpD,IACE,KAAK,YAAY,aAAa;oBAC9B,KAAK,YAAY,gBAAgB;oBACjC,KAAK,YAAY,eAAe,EAChC,CAAC;oBACD,MAAM,CAAC,WAAW,EAAE,CAAC;oBACrB,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["/**\n * The typed facade over `Router` for the intents a URL cannot express (ADR 0003).\n *\n * Angular's `Router` stays the single source of truth: each of these is a line over\n * `router.navigate`, setting `replaceUrl` and a presentation on `NavigationExtras.state`. Deep\n * links, guards and resolvers keep working because it is still the Router, and because state\n * rides in history a back restores the presentation the screen was opened with.\n *\n * A navigation service that owns the stack was rejected: it either re-implements guards,\n * resolvers and deep linking, or it drifts from the URL, and once the stack is not derived from\n * router state the two can disagree.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`,\n * exactly as `NativePlatformLocation` and `NativeStackReuseStrategy` are.\n */\nimport { inject } from '@angular/core';\nimport {\n NavigationCancel,\n NavigationEnd,\n NavigationError,\n Router,\n type NavigationExtras,\n} from '@angular/router';\nimport { NativeBack } from './native-back.ts';\nimport type { ScreenPresentation, StackPresentation } from './screen-presentation.ts';\nimport { leavingPresented, presentedCommands } from './presented-route.ts';\n\n/** What `Router` takes: a url string, or the array form with segments and params. */\nexport type NavigationCommands = string | readonly unknown[];\n\n/**\n * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under\n * one reserved key rather than spread through it.\n */\nexport const NATIVE_INTENT = 'ɵnativeStack';\n\n/** What a navigation asks the outlet to do with the stack, over and above rendering the route. */\nexport interface NativeIntent {\n /** `replace` drops the screen it supersedes; `reset` empties the stack first. */\n readonly stack?: 'push' | 'replace' | 'reset';\n readonly presentation?: ScreenPresentation;\n}\n\nexport interface NativeNavigationOptions extends NavigationExtras {\n /** How the screen arrives: presentation, animation, gesture and sheet props. */\n presentation?: ScreenPresentation;\n}\n\nexport interface PresentOptions extends NativeNavigationOptions {\n /** Shorthand for `presentation.stackPresentation`. Defaults to `modal`. */\n as?: StackPresentation;\n}\n\n/** A url string up to its query or fragment. */\nconst pathOf = (url: string): string => url.split(/[?#]/, 1)[0]!;\n\n/** Read the intent off a navigation's state, if it carries one. */\nexport function intentOf(state: unknown): NativeIntent | null {\n if (!state || typeof state !== 'object') return null;\n const intent = (state as Record<string, unknown>)[NATIVE_INTENT];\n return intent && typeof intent === 'object' ? (intent as NativeIntent) : null;\n}\n\nexport class NativeNavigation {\n private readonly router = inject(Router);\n private readonly outlets = inject(NativeBack);\n\n /**\n * Push a new screen onto the stack. The plain case, and what `nativeRouterLink` does.\n *\n * From a presented screen the new one is presented the same way, over it, since a pushed\n * screen would slide in under it; `presentation` says otherwise, `stackAnimation` and all.\n */\n push(commands: NavigationCommands, options: NativeNavigationOptions = {}): Promise<boolean> {\n return this.go(commands, options, { stack: 'push' });\n }\n\n /**\n * Swap the current screen for another. The history entry is replaced too, so a back from the\n * new screen goes where a back from the old one would have.\n */\n replace(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'replace' });\n }\n\n /**\n * Present a screen over the stack rather than pushing into it: a modal, or a sheet.\n *\n * `present('/filters', { as: 'formSheet', presentation: { sheetAllowedDetents: [0.5, 1] } })`.\n *\n * A presented screen is put on screen outside the stack's navigation controller, so it has no\n * native header and no automatic safe-area insets: it needs its own way out, and its own\n * `safe-area-view`. A sheet can also be dragged away, but `fullScreenModal` cannot, and with\n * `gestureEnabled: false` neither can.\n */\n present(commands: NavigationCommands, options: PresentOptions = {}): Promise<boolean> {\n const { as = 'modal', presentation, ...extras } = options;\n const intent: NativeIntent = {\n stack: 'push',\n presentation: { stackPresentation: as, ...presentation },\n };\n // Asked at once, and only waited for when the page is another tab's: finding its route may\n // load a lazy one.\n const over = this.overTheTabInFront(commands, extras);\n if (!over) return this.go(commands, extras, intent);\n return over.then((found) =>\n this.go(found?.commands ?? commands, found?.extras ?? extras, intent),\n );\n }\n\n /**\n * Replace the whole stack with one screen, the way signing out or finishing onboarding does.\n * Every screen below is destroyed, so there is nothing to go back to.\n */\n reset(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'reset' });\n }\n\n /**\n * Go back exactly as the Android back button does: pop the stack in front, or at the root of a\n * tab's stack, go to the first tab. Not one entry back through history, which after a trip to\n * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.\n */\n back(): void {\n // Optional call: a test that stands a plain object in for the router has no such signal.\n if (!this.router.currentNavigation?.()) {\n this.outlets.back();\n return;\n }\n // Asked while a navigation is still putting its screen up: a page that leaves as it appears,\n // from an effect in its constructor. No stack has that screen to pop yet, so the back waits\n // for the navigation, and goes nowhere if the navigation does not arrive.\n if (this.backWaiting) return;\n this.backWaiting = this.router.events.subscribe((event) => {\n const ended = event instanceof NavigationEnd;\n if (!ended && !(event instanceof NavigationCancel) && !(event instanceof NavigationError)) {\n return;\n }\n this.backWaiting?.unsubscribe();\n this.backWaiting = null;\n if (ended) this.outlets.back();\n });\n }\n\n /** The wait of a back asked for mid-navigation, so two asked for in one are one. */\n private backWaiting: { unsubscribe(): void } | null = null;\n\n /**\n * Pop straight back to the screen at `commands`, taking every screen above it off the stack at\n * once - `popToViewController`. The screen is the one already there, with its state, not a new\n * one. Resolves false, doing nothing, when no screen below the top of a stack in front is\n * showing that url; a push is the way to a screen that is not there.\n */\n popTo(commands: NavigationCommands): Promise<boolean> {\n const tree =\n typeof commands === 'string'\n ? this.router.parseUrl(commands)\n : this.router.createUrlTree([...commands]);\n return this.outlets.popTo(this.router.serializeUrl(tree));\n }\n\n /**\n * Pop the stack in front to its first screen - `popToRootViewController`. Inside a tab, that is\n * the tab's first screen; the screens below keep their state. Resolves false at the root already.\n */\n popToRoot(): Promise<boolean> {\n return this.outlets.popToRoot();\n }\n\n private go(\n commands: NavigationCommands,\n options: NativeNavigationOptions,\n intent: NativeIntent,\n ): Promise<boolean> {\n const { presentation, state, ...extras } = options;\n const full: NativeIntent = presentation\n ? { ...intent, presentation: { ...intent.presentation, ...presentation } }\n : intent;\n\n const navigate = () => {\n const list = typeof commands === 'string' ? [pathOf(commands)] : [...commands];\n const all = {\n ...extras,\n ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),\n state: { ...(state as object), [NATIVE_INTENT]: full },\n };\n const leaving = leavingPresented(this.router, list, all);\n return leaving ? this.router.navigateByUrl(leaving, all) : this.router.navigate(list, all);\n };\n // A screen stacked on the first one waits for it. Navigating while the router's first\n // navigation is still running - a lazily loaded root waiting on its import - cancels that\n // navigation, and the app opens on this screen with nothing beneath it. A reset replaces the\n // stack anyway, so it goes at once.\n const start = () => this.underTabRoot(commands, extras, full, navigate);\n return intent.stack !== 'reset' && this.firstNavigationRunning()\n ? this.firstNavigation().then(start)\n : start();\n }\n\n /**\n * A push into a tab nobody has opened goes to the tab first, so its stack has the tab's own\n * first screen under the page, and a back from the page lands there: what a tab bar with a\n * navigation controller per tab does. Without it the page is the stack's only screen, and the\n * same push leaves a different stack behind depending on whether the tab was ever tapped. A\n * presented screen goes where it is asked, and a replace or a reset has nothing to keep.\n */\n private underTabRoot(\n commands: NavigationCommands,\n extras: NavigationExtras,\n intent: NativeIntent,\n navigate: () => Promise<boolean>,\n ): Promise<boolean> {\n if (intent.stack !== 'push' || intent.presentation) return navigate();\n const tab = this.outlets.unopenedTabOf(this.pathFor(commands, extras));\n if (!tab) return navigate();\n return this.router.navigateByUrl(tab).then((arrived) => (arrived ? navigate() : false));\n }\n\n /** The path the commands lead to, without its query and fragment. */\n private pathFor(commands: NavigationCommands, extras: NavigationExtras): string {\n const url =\n typeof commands === 'string'\n ? commands\n : this.router.serializeUrl(this.router.createUrlTree([...commands], extras));\n return pathOf(url);\n }\n\n /**\n * A page of a tab that is not in front, presented over the one that is: its url would select\n * its own tab and show it there, so it goes to the root's presented outlet instead, where the\n * app's own stack shows it over everything. See `presented-route.ts`. Null for a page of the\n * tab in front, or of no tab, which is presented where its url puts it.\n */\n private overTheTabInFront(\n commands: NavigationCommands,\n extras: NavigationExtras,\n ): Promise<{ commands: unknown[]; extras: NavigationExtras } | null> | null {\n if (!this.outlets.presents) return null;\n const path = this.pathFor(commands, extras);\n if (!this.outlets.inTabBehind(path)) return null;\n const query = typeof commands === 'string' ? this.queryOf(commands, extras) : {};\n return presentedCommands(this.router, path).then(\n (presented) =>\n presented && { commands: presented, extras: { ...extras, ...query, relativeTo: null } },\n );\n }\n\n /**\n * The query and fragment written into a url string, as the extras `navigate` takes them: it\n * reads a string command as one path segment, and would encode a `?` into it. Extras given\n * alongside win over the ones in the string.\n */\n private queryOf(url: string, extras: NavigationExtras): NavigationExtras {\n const cut = url.search(/[?#]/);\n if (cut < 0) return {};\n const { queryParams, fragment } = this.router.parseUrl(url.slice(cut));\n return {\n queryParams: { ...queryParams, ...extras.queryParams },\n fragment: extras.fragment ?? fragment ?? undefined,\n };\n }\n\n private firstNavigationRunning(): boolean {\n const router = this.router as Partial<Router>;\n return router.navigated === false && !!router.currentNavigation?.();\n }\n\n private firstNavigation(): Promise<void> {\n return new Promise((resolve) => {\n const events = this.router.events.subscribe((event) => {\n if (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError\n ) {\n events.unsubscribe();\n resolve();\n }\n });\n });\n }\n}\n"]}
@@ -16,10 +16,10 @@
16
16
  * of its children rather than from a per-screen flag. Toggling that flag is for `ScreenContainer`
17
17
  * (tabs), where every screen is mounted at once.
18
18
  */
19
- import { type ComponentRef, type EnvironmentInjector } from '@angular/core';
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
- export declare class NativeStackOutlet implements RouterOutletContract {
22
+ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
23
23
  /**
24
24
  * Named outlets work exactly as on the web: `<native-stack-outlet name="list" />` is filled by
25
25
  * routes with `outlet: 'list'`. The default is the primary one. Read once, as the router's own
@@ -42,6 +42,10 @@ export declare class NativeStackOutlet implements RouterOutletContract {
42
42
  /** For a refused back to reach the screen as native's own report would. */
43
43
  private readonly engine;
44
44
  private readonly errors;
45
+ /** The screen this stack sits in, when it is a stack inside a page of another. */
46
+ private readonly holder;
47
+ /** What the holder's own `preventNativeDismiss` was, while this stack's top screen refuses. */
48
+ private held;
45
49
  private readonly entries;
46
50
  /**
47
51
  * The screens a replace or a reset supersedes, kept until the navigation that supersedes them
@@ -52,8 +56,33 @@ export declare class NativeStackOutlet implements RouterOutletContract {
52
56
  flex: number;
53
57
  };
54
58
  constructor();
59
+ /** The page presented over everything, while there is one. See `presented-route.ts`. */
60
+ private presentedPage;
61
+ /**
62
+ * The app's own stack, the one in the root component, is also the root's presented outlet: a
63
+ * page of a tab that is not in front is shown here, as a screen over the bar and everything in
64
+ * it. One page at a time, which the router sees to, and never kept: it is dismissed.
65
+ */
66
+ private takePresentedPages;
67
+ private presentPage;
68
+ /** The router left the presented page: take it off the stack. */
69
+ private dropPresentedPage;
70
+ /**
71
+ * Take the router off the presented page, for a back or for a sheet the user swiped away: one
72
+ * entry back where that is the url without the page, and otherwise a navigation to that url in
73
+ * place of this entry, since something under the page was navigated while it was up.
74
+ */
75
+ private dismissPresentedPage;
55
76
  /** The screens currently in the stack, oldest first. Exposed for tests. */
56
77
  get depth(): number;
78
+ ngOnInit(): void;
79
+ /**
80
+ * The route the router is already on, for an outlet created after the navigation that reached
81
+ * it: one held behind a condition until a session or a database is ready. The router activates
82
+ * an outlet as a navigation ends, and that one ended with no outlet to activate, so the outlet
83
+ * takes the route from its context as it arrives, as Angular's own `RouterOutlet` does.
84
+ */
85
+ private activateCurrentRoute;
57
86
  get isActivated(): boolean;
58
87
  get component(): Object | null;
59
88
  get activatedRoute(): ActivatedRoute | null;
@@ -71,6 +100,12 @@ export declare class NativeStackOutlet implements RouterOutletContract {
71
100
  * app's `ErrorHandler` before the router fails the navigation with it.
72
101
  */
73
102
  private createPage;
103
+ /**
104
+ * Take the screen in front off the stack, when there is one: nothing once the router has
105
+ * detached it, as Angular's own outlet does nothing then. A route with no component of its own,
106
+ * a `loadChildren` wrapper or a group, has the router detach the page under it and then
107
+ * deactivate this same outlet, and that page is a screen pushed from, still to come back to.
108
+ */
74
109
  deactivate(): void;
75
110
  /**
76
111
  * Hand the live `ComponentRef` back to the router without destroying it. The screen stays
@@ -167,6 +202,17 @@ export declare class NativeStackOutlet implements RouterOutletContract {
167
202
  * way, through the engine, as if native had sent it.
168
203
  */
169
204
  private refusesButton;
205
+ /**
206
+ * Passes the top screen's refusal to the screen this stack sits in, and that screen's report of
207
+ * an attempt back to the top screen: the swipe that dismisses a presented stack is on the
208
+ * screen holding it, which the page inside cannot bind.
209
+ *
210
+ * Read after every render, since the refusal is a host binding of whatever page is on top.
211
+ */
212
+ private holdRefusals;
213
+ private holdRefusal;
214
+ /** The holder's own answer again, once the top screen no longer refuses. */
215
+ private release;
170
216
  private goBack;
171
217
  /** Whether the top screen is part of what the router shows now. Assumed without a router. */
172
218
  private isShowing;
@@ -194,8 +240,8 @@ export declare class NativeStackOutlet implements RouterOutletContract {
194
240
  private historyUrl;
195
241
  private removeScreen;
196
242
  /**
197
- * What a page is created with: its route, the outlet contexts its own outlets register in, and
198
- * whether its screen is in front - which is this screen's own say and, for a stack inside a
243
+ * What a page is created with: its route, its screen, the outlet contexts its own outlets
244
+ * register in, and whether its screen is in front - which is this screen's own say and, for a stack inside a
199
245
  * screen of another, that screen's too.
200
246
  */
201
247
  private outletInjector;
@@ -16,7 +16,7 @@
16
16
  * of its children rather than from a per-screen flag. Toggling that flag is for `ScreenContainer`
17
17
  * (tabs), where every screen is mounted at once.
18
18
  */
19
- import { ApplicationRef, Component, DestroyRef, ElementRef, ErrorHandler, HostAttributeToken, Injector, Renderer2, computed, createComponent, inject, signal, } from '@angular/core';
19
+ import { ApplicationRef, Component, DestroyRef, ElementRef, ErrorHandler, HostAttributeToken, InjectionToken, Injector, Renderer2, afterEveryRender, computed, createComponent, inject, signal, } from '@angular/core';
20
20
  import { ActivatedRoute, ChildrenOutletContexts, NavigationCancel, NavigationEnd, NavigationError, PRIMARY_OUTLET, Router, createUrlTreeFromSnapshot, } from '@angular/router';
21
21
  import { Location, PlatformLocation } from '@angular/common';
22
22
  import { SCREEN_IN_FRONT } from '@ng-native/device';
@@ -24,8 +24,10 @@ import { bindRouteInputs } from "./bind-route-inputs.js";
24
24
  import { NativeBack, isShowing } from "./native-back.js";
25
25
  import { intentOf } from "./native-navigation.js";
26
26
  import { NativePlatformLocation } from "./native-platform-location.js";
27
+ import { PRESENTED, withoutPresented } from "./presented-route.js";
27
28
  import { markScreenRoute } from "./tab-routes.js";
28
29
  import { ownHost } from "./own-host.js";
30
+ import { SCREEN_HEADER } from "./screen-header.js";
29
31
  import { HostEngine } from '@ng-native/fabric';
30
32
  import { ActivityState } from "./screens.js";
31
33
  import * as i0 from "@angular/core";
@@ -42,6 +44,14 @@ const presented = new WeakMap();
42
44
  const FILL = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 };
43
45
  /** A swipe-back area with no limit on any edge. */
44
46
  const UNLIMITED_SWIPE = { start: -1, end: -1, top: -1, bottom: -1 };
47
+ /**
48
+ * The screen a page lives in, for a stack inside that page. A presented screen with a header is
49
+ * such a stack, and the page that knows whether to refuse a dismissal is a screen inside it, where
50
+ * the screen a swipe down dismisses is this one.
51
+ */
52
+ const HOLDING_SCREEN = new InjectionToken('ng-native.router.holding-screen');
53
+ /** Whether a screen refuses a native dismissal, as its page bound it. */
54
+ const refuses = (screen) => screen?.props?.['preventNativeDismiss'] === true;
45
55
  export class NativeStackOutlet {
46
56
  /**
47
57
  * Named outlets work exactly as on the web: `<native-stack-outlet name="list" />` is filled by
@@ -65,6 +75,10 @@ export class NativeStackOutlet {
65
75
  /** For a refused back to reach the screen as native's own report would. */
66
76
  engine = inject(HostEngine, { optional: true });
67
77
  errors = inject(ErrorHandler);
78
+ /** The screen this stack sits in, when it is a stack inside a page of another. */
79
+ holder = inject(HOLDING_SCREEN, { optional: true });
80
+ /** What the holder's own `preventNativeDismiss` was, while this stack's top screen refuses. */
81
+ held = null;
68
82
  entries = [];
69
83
  /**
70
84
  * The screens a replace or a reset supersedes, kept until the navigation that supersedes them
@@ -102,7 +116,11 @@ export class NativeStackOutlet {
102
116
  else if (event instanceof NavigationError)
103
117
  this.rollBack(event.id);
104
118
  });
119
+ const unhold = this.holder ? this.holdRefusals(this.holder) : undefined;
120
+ const unpresent = this.takePresentedPages(back);
105
121
  inject(DestroyRef).onDestroy(() => {
122
+ unhold?.();
123
+ unpresent?.();
106
124
  unsubscribeBack();
107
125
  unsubscribeStack();
108
126
  navigations?.unsubscribe();
@@ -114,10 +132,125 @@ export class NativeStackOutlet {
114
132
  this.parentContexts.onChildOutletDestroyed(this.name);
115
133
  });
116
134
  }
135
+ /** The page presented over everything, while there is one. See `presented-route.ts`. */
136
+ presentedPage = null;
137
+ /**
138
+ * The app's own stack, the one in the root component, is also the root's presented outlet: a
139
+ * page of a tab that is not in front is shown here, as a screen over the bar and everything in
140
+ * it. One page at a time, which the router sees to, and never kept: it is dismissed.
141
+ */
142
+ takePresentedPages(back) {
143
+ const router = this.router;
144
+ const root = inject(ActivatedRoute, { optional: true });
145
+ if (!router?.routerState || this.name !== PRIMARY_OUTLET || root !== router.routerState.root) {
146
+ return undefined;
147
+ }
148
+ const page = () => this.presentedPage;
149
+ const outlet = {
150
+ supportsBindingToComponentInputs: true,
151
+ get isActivated() {
152
+ return page() !== null;
153
+ },
154
+ get component() {
155
+ return page()?.ref.instance ?? null;
156
+ },
157
+ get activatedRoute() {
158
+ return page()?.route ?? null;
159
+ },
160
+ get activatedRouteData() {
161
+ return page()?.route.snapshot.data ?? {};
162
+ },
163
+ activateWith: (route, environmentInjector) => this.presentPage(route, environmentInjector, back),
164
+ deactivate: () => this.dropPresentedPage(),
165
+ detach: () => {
166
+ throw new Error('[angular-native] a presented page is dismissed, not detached');
167
+ },
168
+ attach: () => {
169
+ throw new Error('[angular-native] a presented page is not kept to attach again');
170
+ },
171
+ };
172
+ this.parentContexts.onChildOutletCreated(PRESENTED, outlet);
173
+ back.presents = true;
174
+ return () => {
175
+ back.presents = false;
176
+ this.dropPresentedPage();
177
+ this.parentContexts.onChildOutletDestroyed(PRESENTED);
178
+ };
179
+ }
180
+ presentPage(route, environmentInjector, back) {
181
+ const component = (route.component ?? route.snapshot.component);
182
+ if (!component)
183
+ throw new Error('[angular-native] route has no component to present');
184
+ const presentation = this.claimPresentation(this.currentIntent()) ?? {
185
+ stackPresentation: 'modal',
186
+ };
187
+ const screen = this.createScreen(presentation, () => this.dismissPresentedPage());
188
+ const inFront = signal(true, /* @ts-ignore */
189
+ ...(ngDevMode ? [{ debugName: "inFront" }] : /* istanbul ignore next */ []));
190
+ const { ref, unbind } = this.createPage(component, route, environmentInjector, screen, inFront, PRESENTED);
191
+ this.fillScreen(screen);
192
+ // Subscribed now, so it is the newest answer and the first asked: a back dismisses the page
193
+ // before it pops anything in the tab under it.
194
+ const stopBack = back.handle(() => this.dismissPresentedPage());
195
+ this.presentedPage = {
196
+ route,
197
+ ref,
198
+ screen,
199
+ detached: false,
200
+ unbind,
201
+ presentation,
202
+ inFront,
203
+ stopBack,
204
+ };
205
+ }
206
+ /** The router left the presented page: take it off the stack. */
207
+ dropPresentedPage() {
208
+ const page = this.presentedPage;
209
+ if (!page)
210
+ return;
211
+ this.presentedPage = null;
212
+ page.stopBack();
213
+ this.drop(page);
214
+ }
215
+ /**
216
+ * Take the router off the presented page, for a back or for a sheet the user swiped away: one
217
+ * entry back where that is the url without the page, and otherwise a navigation to that url in
218
+ * place of this entry, since something under the page was navigated while it was up.
219
+ */
220
+ dismissPresentedPage() {
221
+ if (!this.presentedPage || !this.router)
222
+ return false;
223
+ const without = withoutPresented(this.router, this.router.url);
224
+ if (this.historyUrl(-1) === without)
225
+ this.location.historyGo(-1);
226
+ else
227
+ void this.router.navigateByUrl(without, { replaceUrl: true });
228
+ return true;
229
+ }
117
230
  /** The screens currently in the stack, oldest first. Exposed for tests. */
118
231
  get depth() {
119
232
  return this.entries.length;
120
233
  }
234
+ ngOnInit() {
235
+ this.activateCurrentRoute();
236
+ }
237
+ /**
238
+ * The route the router is already on, for an outlet created after the navigation that reached
239
+ * it: one held behind a condition until a session or a database is ready. The router activates
240
+ * an outlet as a navigation ends, and that one ended with no outlet to activate, so the outlet
241
+ * takes the route from its context as it arrives, as Angular's own `RouterOutlet` does.
242
+ */
243
+ activateCurrentRoute() {
244
+ if (this.isActivated)
245
+ return;
246
+ const context = this.parentContexts.getContext(this.name);
247
+ if (!context?.route)
248
+ return;
249
+ if (context.attachRef)
250
+ this.attach(context.attachRef, context.route);
251
+ else
252
+ this.activateWith(context.route, context.injector);
253
+ }
121
254
  // --- RouterOutletContract ----------------------------------------------
122
255
  get isActivated() {
123
256
  return this.top !== undefined && !this.top.detached;
@@ -188,13 +321,13 @@ export class NativeStackOutlet {
188
321
  * screen it was pushed from given back its change detection, and the error handed to the
189
322
  * app's `ErrorHandler` before the router fails the navigation with it.
190
323
  */
191
- createPage(component, route, environmentInjector, screen, inFront) {
324
+ createPage(component, route, environmentInjector, screen, inFront, outlet = this.name) {
192
325
  let ref;
193
326
  let unbind;
194
327
  try {
195
328
  ref = createComponent(component, {
196
329
  environmentInjector,
197
- elementInjector: this.outletInjector(route, inFront),
330
+ elementInjector: this.outletInjector(route, inFront, screen, outlet),
198
331
  hostElement: screen,
199
332
  });
200
333
  this.applicationRef.attachView(ref.hostView);
@@ -219,11 +352,16 @@ export class NativeStackOutlet {
219
352
  throw error;
220
353
  }
221
354
  }
355
+ /**
356
+ * Take the screen in front off the stack, when there is one: nothing once the router has
357
+ * detached it, as Angular's own outlet does nothing then. A route with no component of its own,
358
+ * a `loadChildren` wrapper or a group, has the router detach the page under it and then
359
+ * deactivate this same outlet, and that page is a screen pushed from, still to come back to.
360
+ */
222
361
  deactivate() {
223
- const entry = this.entries.pop();
224
- if (!entry)
362
+ if (!this.isActivated)
225
363
  return;
226
- this.drop(entry);
364
+ this.drop(this.entries.pop());
227
365
  }
228
366
  /**
229
367
  * Hand the live `ComponentRef` back to the router without destroying it. The screen stays
@@ -414,7 +552,7 @@ export class NativeStackOutlet {
414
552
  this.renderer.setStyle(screen, key, value);
415
553
  }
416
554
  }
417
- createScreen(presentation) {
555
+ createScreen(presentation, dismissed = (count) => this.onNativeDismiss(count)) {
418
556
  const screen = ownHost(this.renderer.createElement('screen'));
419
557
  this.fillScreen(screen);
420
558
  // Set once and never lowered: see the note at the top of this file.
@@ -422,7 +560,7 @@ export class NativeStackOutlet {
422
560
  // A swipe-back or an Android back button pops the screen natively and only then tells JS.
423
561
  // Without this the native stack and the router disagree from that moment on: the screen is
424
562
  // gone but the URL still points at it, so the next navigation starts from the wrong place.
425
- this.renderer.listen(screen, 'dismissed', (event) => this.onNativeDismiss(event?.nativeEvent?.dismissCount ?? 1));
563
+ this.renderer.listen(screen, 'dismissed', (event) => dismissed(event?.nativeEvent?.dismissCount ?? 1));
426
564
  // Every key is a prop `RNSScreen` already declares, spelled the way the codegen spec spells
427
565
  // it, so this forwards rather than translates. Set before the screen joins the stack: the
428
566
  // native side reads the presentation as it mounts.
@@ -478,6 +616,50 @@ export class NativeStackOutlet {
478
616
  native?.dispatchEvent?.(screen, 'topNativeDismissCancelled', { dismissCount: 1 });
479
617
  return true;
480
618
  }
619
+ /**
620
+ * Passes the top screen's refusal to the screen this stack sits in, and that screen's report of
621
+ * an attempt back to the top screen: the swipe that dismisses a presented stack is on the
622
+ * screen holding it, which the page inside cannot bind.
623
+ *
624
+ * Read after every render, since the refusal is a host binding of whatever page is on top.
625
+ */
626
+ holdRefusals(holder) {
627
+ const render = afterEveryRender(() => this.holdRefusal(holder));
628
+ const unlisten = this.renderer.listen(holder.screen, 'nativeDismissCancelled', (event) => {
629
+ // The stack can have lost its last screen with the refusal not yet released.
630
+ const top = this.top;
631
+ if (!this.held || !top)
632
+ return;
633
+ const native = this.engine;
634
+ const report = event?.nativeEvent ?? { dismissCount: 1 };
635
+ native?.dispatchEvent?.(top.screen, 'topNativeDismissCancelled', report);
636
+ });
637
+ return () => {
638
+ render.destroy();
639
+ unlisten();
640
+ this.release(holder);
641
+ };
642
+ }
643
+ holdRefusal(holder) {
644
+ // A stack in a tab behind is inside the same screen, and has no say in it.
645
+ if (!refuses(this.top?.screen) || !this.isShowing())
646
+ return this.release(holder);
647
+ if (refuses(holder.screen))
648
+ return;
649
+ // ponytail: the holder's own binding is read when the top screen starts refusing. One that
650
+ // turns true while it does is put back as it was then; bind the refusal in one of the two.
651
+ this.held = { own: holder.screen.props?.['preventNativeDismiss'] };
652
+ this.renderer.setProperty(holder.screen, 'preventNativeDismiss', true);
653
+ holder.refusalChanged();
654
+ }
655
+ /** The holder's own answer again, once the top screen no longer refuses. */
656
+ release(holder) {
657
+ if (!this.held)
658
+ return;
659
+ this.renderer.setProperty(holder.screen, 'preventNativeDismiss', this.held.own ?? false);
660
+ this.held = null;
661
+ holder.refusalChanged();
662
+ }
481
663
  goBack() {
482
664
  if (this.entries.length < 2 || !this.isShowing())
483
665
  return false;
@@ -561,20 +743,28 @@ export class NativeStackOutlet {
561
743
  this.renderer.removeChild(this.host.nativeElement, screen);
562
744
  }
563
745
  /**
564
- * What a page is created with: its route, the outlet contexts its own outlets register in, and
565
- * whether its screen is in front - which is this screen's own say and, for a stack inside a
746
+ * What a page is created with: its route, its screen, the outlet contexts its own outlets
747
+ * register in, and whether its screen is in front - which is this screen's own say and, for a stack inside a
566
748
  * screen of another, that screen's too.
567
749
  */
568
- outletInjector(route, inFront) {
750
+ outletInjector(route, inFront, screen, outlet = this.name) {
569
751
  const outer = this.injector.get(SCREEN_IN_FRONT);
570
752
  return Injector.create({
571
753
  parent: this.injector,
572
754
  providers: [
573
755
  { provide: SCREEN_IN_FRONT, useValue: computed(() => outer() && inFront()) },
756
+ {
757
+ provide: HOLDING_SCREEN,
758
+ useValue: {
759
+ screen,
760
+ refusalChanged: () => this.holder && this.holdRefusal(this.holder),
761
+ },
762
+ },
574
763
  { provide: ActivatedRoute, useValue: route },
764
+ { provide: SCREEN_HEADER, useValue: signal(false) },
575
765
  {
576
766
  provide: ChildrenOutletContexts,
577
- useValue: this.parentContexts.getOrCreateContext(this.name).children,
767
+ useValue: this.parentContexts.getOrCreateContext(outlet).children,
578
768
  },
579
769
  ],
580
770
  });