@ng-native/router 0.5.0 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"native-stack-reuse-strategy.js","sourceRoot":"","sources":["../src/native-stack-reuse-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAqB,MAAM,eAAe,CAAC;AACpE,OAAO,EACL,sBAAsB,EAGtB,MAAM,GAEP,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE5D;;;;GAIG;AACH,MAAM,YAAY,GAAG,cAAc,CAAC;AAEpC;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAkB,KAAQ;IACnD,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;AACrE,CAAC;AAOD;;;;;;;GAOG;AACH,SAAS,KAAK,CAAC,KAA6B;IAC1C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,IAAI,OAAO,GAAkC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QAC3F,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,OAAO,wBAAyB,SAAQ,sBAAsB;IAClE;;;OAGG;IACc,OAAO,GAAG,IAAI,GAAG,EAAiC,CAAC;IACpE;;;OAGG;IACc,QAAQ,GAAG,YAAY,EAAE,CAAC;IAE3C;;;;;;;;;;OAUG;IACM,YAAY,CAAC,KAA6B;QACjD,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC3C,kFAAkF;QAClF,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAC7C,OAAO,CACL,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;YAC3B,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC;YAC7B,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CACjC,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,KAA6B,EAAE,MAAkC;QAC9E,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACzC,IAAI,MAAM,EAAE,CAAC;YACX,6EAA6E;YAC7E,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YACzE,IAAI,CAAC,eAAe,CAAE,MAAuB,CAAC,YAAY,CAAC,CAAC;YAC5D,OAAO;QACT,CAAC;QACD,yEAAyE;QACzE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;;YACzD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAEgB,OAAO,GAAG,IAAI,OAAO,EAAyB,CAAC;IAEhE;;;;;;;;OAQG;IACK,eAAe,CAAC,GAAsC;QAC5D,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO;QAC1C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YACjB,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACvC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAE,MAAuB,CAAC,YAAY,KAAK,GAAG,CAAC,CAAC;gBACpF,IAAI,IAAI,CAAC,MAAM;oBAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;;oBACxC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChC,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;OAQG;IACM,gBAAgB,CAAC,MAA8B,EAAE,IAA4B;QACpF,IAAI,MAAM,CAAC,WAAW,KAAK,IAAI,CAAC,WAAW;YAAE,OAAO,KAAK,CAAC;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;QAChC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,EAAE,IAAI,EAAE,CAAC,YAAY,CAAC,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACjF,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IAED;;;;;;;;OAQG;IACM,YAAY,CAAC,KAA6B;QACjD,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO,KAAK,CAAC;QAC7D,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;IACvC,CAAC;IAEO,OAAO;QACb,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,iBAAiB,EAAE,CAAC;QAClE,2FAA2F;QAC3F,wCAAwC;QACxC,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,OAAO,KAAK,UAAU;YAAE,OAAO,KAAK,CAAC;QACnE,OAAO,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,MAAM,CAAC;IAC7D,CAAC;IAEQ,QAAQ,CAAC,KAA6B;QAC7C,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAC/C,CAAC,MAAM,EAAE,EAAE,CAAC,CAAE,MAAuB,CAAC,YAAY,EAAE,QAAQ,CAAC,SAAS,CACvE,CAAC;QACF,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;;YACxC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,mEAAmE;QACnE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC7B,CAAC;CACF","sourcesContent":["/**\n * The reuse strategy that turns navigation into a stack.\n *\n * Angular's default strategy never detaches, so navigating away destroys the component and its\n * native views, and coming back rebuilds them from scratch: a list is back at the top, a form is\n * empty. Detaching instead hands the live `ComponentRef` to the outlet, which keeps its screen\n * mounted. That is the whole reason `RouterOutletContract.detach()` exists.\n *\n * No decorators, so tests can import it and so it can be provided with `useClass`.\n */\nimport { Injector, inject, type ComponentRef } from '@angular/core';\nimport {\n BaseRouteReuseStrategy,\n type ActivatedRouteSnapshot,\n type DetachedRouteHandle,\n Router,\n type Route,\n} from '@angular/router';\nimport { intentOf } from './native-navigation.ts';\nimport { PRESENTED } from './presented-route.ts';\nimport { isScreenRoute, isTabRoute } from './tab-routes.ts';\n\n/**\n * Where `reuseScreen` marks a route. On the config's `data` rather than in a set of configs,\n * because the router copies every route config it is given, so the object an app wrote is never\n * the one a snapshot points at.\n */\nconst REUSE_SCREEN = 'ɵreuseScreen';\n\n/**\n * Keep one screen for a route whatever its parameters, updating it in place as Angular's own\n * strategy does, instead of pushing a new screen for each url.\n *\n * `reuseScreen({ path: 'photo/:index', component: Photo })` for a route whose parameter picks\n * what one screen shows - a pager, a step through a list - rather than naming another screen to\n * go to. Everything else stacks: `/user/1` then `/user/2` is two screens, and back returns to the\n * first.\n */\nexport function reuseScreen<T extends Route>(route: T): T {\n return { ...route, data: { ...route.data, [REUSE_SCREEN]: true } };\n}\n\n/** What the router actually stores: `DetachedRouteHandle` is typed as an opaque object. */\ninterface StoredHandle {\n readonly componentRef?: ComponentRef<unknown>;\n}\n\n/**\n * Identify a screen by its resolved url rather than its route config, so `/user/1` and `/user/2`\n * are two screens on the stack rather than one that gets reused.\n *\n * One entry per level of the route tree, empty ones included. Flattened to a url, a tab at\n * `/tabs/library` and the list at path `''` beneath it are the same string, and the router, asking\n * for the list, is handed the tab's whole tree and makes the tab its own child.\n */\nfunction keyOf(route: ActivatedRouteSnapshot): string {\n const levels: string[] = [];\n for (let current: ActivatedRouteSnapshot | null = route; current; current = current.parent) {\n levels.unshift(current.url.map((segment) => segment.toString()).join('/'));\n }\n return JSON.stringify(levels);\n}\n\nfunction injectorHere(): Injector | null {\n try {\n return inject(Injector);\n } catch {\n return null;\n }\n}\n\nexport class NativeStackReuseStrategy extends BaseRouteReuseStrategy {\n /**\n * The screens kept for each url, oldest first. More than one where the same url is on the stack\n * twice: a customer, one of its jobs, and the customer again from the job.\n */\n private readonly handles = new Map<string, DetachedRouteHandle[]>();\n /**\n * For the navigation in flight, read when it is asked for: the router is built from this. Null\n * where the strategy is made by hand, outside an injector, and every navigation is then plain.\n */\n private readonly injector = injectorHere();\n\n /**\n * Only leaf routes become screens; a parent with children is layout, not a destination.\n *\n * A tab is the exception, and has to be. Detaching only its leaf would rebuild the tab itself\n * on every switch, and with it the stack outlet inside: a tab two screens deep would come back\n * one screen deep, having lost the screens the user could go back to. Detaching the tab keeps\n * its whole subtree, which is what makes each tab remember where it was.\n *\n * So is a parent a native stack put on screen, such as a root stack's tab bar: a sheet presented\n * over it has to leave it standing (see `markScreenRoute`).\n */\n override shouldDetach(route: ActivatedRouteSnapshot): boolean {\n if (route.component === null) return false;\n // A page presented over the tabs is dismissed, not kept: nothing goes back to it.\n if (route.outlet === PRESENTED) return false;\n return (\n route.children.length === 0 ||\n isTabRoute(route.routeConfig) ||\n isScreenRoute(route.routeConfig)\n );\n }\n\n override store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {\n const key = keyOf(route);\n const kept = this.handles.get(key) ?? [];\n if (handle) {\n // The same screen stored again takes its place at the end, not a second one.\n this.handles.set(key, [...kept.filter((one) => one !== handle), handle]);\n this.forgetOnDestroy((handle as StoredHandle).componentRef);\n return;\n }\n // The router has re-attached the one `retrieve` answered with: the last.\n if (kept.length > 1) this.handles.set(key, kept.slice(0, -1));\n else this.handles.delete(key);\n }\n\n private readonly watched = new WeakSet<ComponentRef<unknown>>();\n\n /**\n * Drop a screen's handles the moment its component is destroyed.\n *\n * The router stores a handle for every screen it navigates away from, the one being popped\n * included, and the outlet then destroys that screen. A handle outlives its component, and holds\n * the component's whole view and every native node in it; `retrieve` notices only when the same\n * url is asked for again, so a url never revisited kept its last screen for as long as the app\n * ran.\n */\n private forgetOnDestroy(ref: ComponentRef<unknown> | undefined): void {\n if (!ref || this.watched.has(ref)) return;\n this.watched.add(ref);\n ref.onDestroy(() => {\n for (const [key, kept] of this.handles) {\n const left = kept.filter((handle) => (handle as StoredHandle).componentRef !== ref);\n if (left.length) this.handles.set(key, left);\n else this.handles.delete(key);\n }\n });\n }\n\n /**\n * Angular reuses a route whenever its config is the same, so on the web `/user/1` to `/user/2`\n * keeps the one component and feeds it the new params. On a native stack that turns a push into\n * an update in place: no push animation, one screen, and back skipping past `/user/1`. A screen\n * a stack put on screen is reused only when its url is the same too, so a different parameter\n * is a different screen, which the router detaches and stores as it does any screen pushed\n * away. A query or fragment change is not part of the url that identifies a screen, and a\n * parent that is only layout is reused as before, so its stack keeps what is in it.\n */\n override shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {\n if (future.routeConfig !== curr.routeConfig) return false;\n const config = curr.routeConfig;\n if (!isScreenRoute(config) || config?.data?.[REUSE_SCREEN] === true) return true;\n return keyOf(future) === keyOf(curr);\n }\n\n /**\n * Whether the router goes back to a kept screen for this url. Not for a push: a push to a url\n * already on the stack is a new screen over it, as a native stack makes one, and Back returns\n * to where the push came from. Going back to the kept one is a back, or `popTo`.\n *\n * Never for a route with no component, which is never kept: the router asks for every route,\n * and a group at path `''` has the same url as a page at `''` beside it, whose screen it would\n * otherwise be handed.\n */\n override shouldAttach(route: ActivatedRouteSnapshot): boolean {\n if (route.component === null || this.pushing()) return false;\n return this.retrieve(route) !== null;\n }\n\n private pushing(): boolean {\n const navigation = this.injector?.get(Router).currentNavigation();\n // A back restores the state its history entry was pushed with, intent and all: that is the\n // screen being returned to, not a push.\n if (!navigation || navigation.trigger === 'popstate') return false;\n return intentOf(navigation.extras.state)?.stack === 'push';\n }\n\n override retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {\n const key = keyOf(route);\n const kept = (this.handles.get(key) ?? []).filter(\n (handle) => !(handle as StoredHandle).componentRef?.hostView.destroyed,\n );\n if (kept.length) this.handles.set(key, kept);\n else this.handles.delete(key);\n // The newest: the one a back from the top of the stack returns to.\n return kept.at(-1) ?? null;\n }\n}\n"]}
1
+ {"version":3,"file":"native-stack-reuse-strategy.js","sourceRoot":"","sources":["../src/native-stack-reuse-strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The reuse strategy that turns navigation into a stack.\n *\n * Angular's default strategy never detaches, so navigating away destroys the component and its\n * native views, and coming back rebuilds them from scratch: a list is back at the top, a form is\n * empty. Detaching instead hands the live `ComponentRef` to the outlet, which keeps its screen\n * mounted. That is the whole reason `RouterOutletContract.detach()` exists.\n *\n * No decorators, so tests can import it and so it can be provided with `useClass`.\n */\nimport { Injector, inject, type ComponentRef } from '@angular/core';\nimport {\n BaseRouteReuseStrategy,\n type ActivatedRouteSnapshot,\n type DetachedRouteHandle,\n Router,\n type Route,\n} from '@angular/router';\nimport { intentOf } from './native-navigation.ts';\nimport { PRESENTED } from './presented-route.ts';\nimport { isScreenRoute, isTabRoute } from './tab-routes.ts';\n\n/**\n * Where `reuseScreen` marks a route. On the config's `data` rather than in a set of configs,\n * because the router copies every route config it is given, so the object an app wrote is never\n * the one a snapshot points at.\n */\nconst REUSE_SCREEN = 'ɵreuseScreen';\n\n/**\n * Keep one screen for a route whatever its parameters, updating it in place as Angular's own\n * strategy does, instead of pushing a new screen for each url.\n *\n * `reuseScreen({ path: 'photo/:index', component: Photo })` for a route whose parameter picks\n * what one screen shows - a pager, a step through a list - rather than naming another screen to\n * go to. Everything else stacks: `/user/1` then `/user/2` is two screens, and back returns to the\n * first.\n */\nexport function reuseScreen<T extends Route>(route: T): T {\n return { ...route, data: { ...route.data, [REUSE_SCREEN]: true } };\n}\n\n/** What the router actually stores: `DetachedRouteHandle` is typed as an opaque object. */\ninterface StoredHandle {\n readonly componentRef?: ComponentRef<unknown>;\n}\n\n/**\n * Identify a screen by its resolved url rather than its route config, so `/user/1` and `/user/2`\n * are two screens on the stack rather than one that gets reused.\n *\n * One entry per level of the route tree, empty ones included. Flattened to a url, a tab at\n * `/tabs/library` and the list at path `''` beneath it are the same string, and the router, asking\n * for the list, is handed the tab's whole tree and makes the tab its own child.\n */\nfunction keyOf(route: ActivatedRouteSnapshot): string {\n const levels: string[] = [];\n for (let current: ActivatedRouteSnapshot | null = route; current; current = current.parent) {\n levels.unshift(current.url.map((segment) => segment.toString()).join('/'));\n }\n return JSON.stringify(levels);\n}\n\nfunction injectorHere(): Injector | null {\n try {\n return inject(Injector);\n } catch {\n return null;\n }\n}\n\nexport class NativeStackReuseStrategy extends BaseRouteReuseStrategy {\n /**\n * The screens kept for each url, oldest first. More than one where the same url is on the stack\n * twice: a customer, one of its jobs, and the customer again from the job.\n */\n private readonly handles = new Map<string, DetachedRouteHandle[]>();\n /**\n * For the navigation in flight, read when it is asked for: the router is built from this. Null\n * where the strategy is made by hand, outside an injector, and every navigation is then plain.\n */\n private readonly injector = injectorHere();\n\n /**\n * Only leaf routes become screens; a parent with children is layout, not a destination.\n *\n * A tab is the exception, and has to be. Detaching only its leaf would rebuild the tab itself\n * on every switch, and with it the stack outlet inside: a tab two screens deep would come back\n * one screen deep, having lost the screens the user could go back to. Detaching the tab keeps\n * its whole subtree, which is what makes each tab remember where it was.\n *\n * So is a parent a native stack put on screen, such as a root stack's tab bar: a sheet presented\n * over it has to leave it standing (see `markScreenRoute`).\n */\n override shouldDetach(route: ActivatedRouteSnapshot): boolean {\n if (route.component === null) return false;\n // A page presented over the tabs is dismissed, not kept: nothing goes back to it.\n if (route.outlet === PRESENTED) return false;\n return (\n route.children.length === 0 ||\n isTabRoute(route.routeConfig) ||\n isScreenRoute(route.routeConfig)\n );\n }\n\n override store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {\n const key = keyOf(route);\n const kept = this.handles.get(key) ?? [];\n if (handle) {\n // The same screen stored again takes its place at the end, not a second one.\n this.handles.set(key, [...kept.filter((one) => one !== handle), handle]);\n this.forgetOnDestroy((handle as StoredHandle).componentRef);\n return;\n }\n // The router has re-attached the one `retrieve` answered with: the last.\n if (kept.length > 1) this.handles.set(key, kept.slice(0, -1));\n else this.handles.delete(key);\n }\n\n private readonly watched = new WeakSet<ComponentRef<unknown>>();\n\n /**\n * Drop a screen's handles the moment its component is destroyed.\n *\n * The router stores a handle for every screen it navigates away from, the one being popped\n * included, and the outlet then destroys that screen. A handle outlives its component, and holds\n * the component's whole view and every native node in it; `retrieve` notices only when the same\n * url is asked for again, so a url never revisited kept its last screen for as long as the app\n * ran.\n */\n private forgetOnDestroy(ref: ComponentRef<unknown> | undefined): void {\n if (!ref || this.watched.has(ref)) return;\n this.watched.add(ref);\n ref.onDestroy(() => {\n for (const [key, kept] of this.handles) {\n const left = kept.filter((handle) => (handle as StoredHandle).componentRef !== ref);\n if (left.length) this.handles.set(key, left);\n else this.handles.delete(key);\n }\n });\n }\n\n /**\n * Angular reuses a route whenever its config is the same, so on the web `/user/1` to `/user/2`\n * keeps the one component and feeds it the new params. On a native stack that turns a push into\n * an update in place: no push animation, one screen, and back skipping past `/user/1`. A screen\n * a stack put on screen is reused only when its url is the same too, so a different parameter\n * is a different screen, which the router detaches and stores as it does any screen pushed\n * away. A query or fragment change is not part of the url that identifies a screen, and a\n * parent that is only layout is reused as before, so its stack keeps what is in it.\n */\n override shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {\n if (future.routeConfig !== curr.routeConfig) return false;\n const config = curr.routeConfig;\n const reuse =\n !isScreenRoute(config) ||\n config?.data?.[REUSE_SCREEN] === true ||\n keyOf(future) === keyOf(curr);\n if (reuse) this.reused.add(future);\n return reuse;\n }\n\n /** The routes the navigation in flight keeps as they are, by the snapshot it is building. */\n private readonly reused = new WeakSet<ActivatedRouteSnapshot>();\n\n /**\n * Whether the router goes back to a kept screen for this url. Not for a push: a push to a url\n * already on the stack is a new screen over it, as a native stack makes one, and Back returns\n * to where the push came from. Going back to the kept one is a back, or `popTo`.\n *\n * Never for a route with no component, which is never kept: the router asks for every route,\n * and a group at path `''` has the same url as a page at `''` beside it, whose screen it would\n * otherwise be handed.\n *\n * A tab is attached for a push too: the push is into the tab's own stack, which is kept with it,\n * and building the tab again would leave a second stack in the tab's screen. Only into the bar\n * already showing, though: a push that builds another bar over it builds its tabs too, and the\n * one kept at the same url is the bar beneath's.\n */\n override shouldAttach(route: ActivatedRouteSnapshot): boolean {\n if (route.component === null) return false;\n if (this.pushing() && !(isTabRoute(route.routeConfig) && this.inKeptBar(route))) return false;\n return this.retrieve(route) !== null;\n }\n\n /** Whether the tab bar above a tab's route is one the navigation keeps rather than builds. */\n private inKeptBar(route: ActivatedRouteSnapshot): boolean {\n let bar = route.parent;\n while (bar && bar.component === null) bar = bar.parent;\n return bar !== null && this.reused.has(bar);\n }\n\n private pushing(): boolean {\n const navigation = this.injector?.get(Router).currentNavigation();\n // A back restores the state its history entry was pushed with, intent and all: that is the\n // screen being returned to, not a push.\n if (!navigation || navigation.trigger === 'popstate') return false;\n return intentOf(navigation.extras.state)?.stack === 'push';\n }\n\n override retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {\n const key = keyOf(route);\n const kept = (this.handles.get(key) ?? []).filter(\n (handle) => !(handle as StoredHandle).componentRef?.hostView.destroyed,\n );\n if (kept.length) this.handles.set(key, kept);\n else this.handles.delete(key);\n // The newest: the one a back from the top of the stack returns to.\n return kept.at(-1) ?? null;\n }\n}\n"]}
@@ -117,6 +117,11 @@ export declare function tabIconProps(icon: TabIconProps, selectedIcon: TabIconPr
117
117
  selectedDrawableIconResourceName?: string;
118
118
  selectedImageIconResource?: unknown;
119
119
  };
120
+ /**
121
+ * The key native identifies a tab by: its path, or `/` for the tab at path `''`, since iOS reads an
122
+ * empty key as none and fails on the tap that selects it. No route path starts with `/`.
123
+ */
124
+ export declare function screenKeyOf(path: string): string;
120
125
  export declare class NativeTab implements OnInit {
121
126
  private readonly engine;
122
127
  /** The app's `withTabDefaults`, for an appearance this tab does not bind itself. */
@@ -132,6 +137,7 @@ export declare class NativeTab implements OnInit {
132
137
  * identifies the tab by it too, so it must be unique in the bar.
133
138
  */
134
139
  readonly path: import("@angular/core").InputSignal<string>;
140
+ protected readonly screenKey: import("@angular/core").Signal<string>;
135
141
  /** The label under the icon. Absent gives an icon-only item. */
136
142
  readonly title: import("@angular/core").InputSignal<string | undefined>;
137
143
  /** iOS: an SF Symbol name, e.g. `books.vertical.fill`. Shorthand for `[icon]`. */
@@ -83,6 +83,13 @@ function warnIfIconOnOnePlatform(tab) {
83
83
  console.warn(`[angular-native] <native-tab path="${path}"> has no icon on ${platform}: ${platform} reads ` +
84
84
  `only ${missing}. Add ${missing}="...", or bind [icon] for a tab meant for one platform.`);
85
85
  }
86
+ /**
87
+ * The key native identifies a tab by: its path, or `/` for the tab at path `''`, since iOS reads an
88
+ * empty key as none and fails on the tap that selects it. No route path starts with `/`.
89
+ */
90
+ export function screenKeyOf(path) {
91
+ return path || '/';
92
+ }
86
93
  export class NativeTab {
87
94
  engine = inject(HostEngine);
88
95
  /** The app's `withTabDefaults`, for an appearance this tab does not bind itself. */
@@ -101,6 +108,8 @@ export class NativeTab {
101
108
  */
102
109
  path = input.required(/* @ts-ignore */
103
110
  ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
111
+ screenKey = computed(() => screenKeyOf(this.path()), /* @ts-ignore */
112
+ ...(ngDevMode ? [{ debugName: "screenKey" }] : /* istanbul ignore next */ []));
104
113
  /** The label under the icon. Absent gives an icon-only item. */
105
114
  title = input(/* @ts-ignore */
106
115
  ...(ngDevMode ? [undefined, { debugName: "title" }] : /* istanbul ignore next */ []));
@@ -209,7 +218,7 @@ export class NativeTab {
209
218
  };
210
219
  }
211
220
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: NativeTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
212
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: NativeTab, isStandalone: true, selector: "native-tab", inputs: { path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, sfSymbol: { classPropertyName: "sfSymbol", publicName: "sfSymbol", isSignal: true, isRequired: false, transformFunction: null }, drawable: { classPropertyName: "drawable", publicName: "drawable", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, selectedIcon: { classPropertyName: "selectedIcon", publicName: "selectedIcon", isSignal: true, isRequired: false, transformFunction: null }, systemItem: { classPropertyName: "systemItem", publicName: "systemItem", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, accessibilityLabel: { classPropertyName: "accessibilityLabel", publicName: "accessibilityLabel", isSignal: true, isRequired: false, transformFunction: null }, standardAppearance: { classPropertyName: "standardAppearance", publicName: "standardAppearance", isSignal: true, isRequired: false, transformFunction: null }, scrollEdgeAppearance: { classPropertyName: "scrollEdgeAppearance", publicName: "scrollEdgeAppearance", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "screenKey": "path()", "title": "title()", "badgeValue": "badge()", "tabBarItemAccessibilityLabel": "accessibilityLabel()", "systemItem": "systemItem()", "isTitleUndefined": "title() === undefined", "iconType": "icons().iconType", "iconResourceName": "icons().iconResourceName", "iconImageSource": "icons().iconImageSource", "selectedIconResourceName": "icons().selectedIconResourceName", "selectedIconImageSource": "icons().selectedIconImageSource", "drawableIconResourceName": "icons().drawableIconResourceName", "imageIconResource": "icons().imageIconResource", "selectedDrawableIconResourceName": "icons().selectedDrawableIconResourceName", "selectedImageIconResource": "icons().selectedImageIconResource", "standardAppearance": "standard()", "scrollEdgeAppearance": "scrollEdge()" } }, ngImport: i0, template: '', isInline: true });
221
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: NativeTab, isStandalone: true, selector: "native-tab", inputs: { path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, sfSymbol: { classPropertyName: "sfSymbol", publicName: "sfSymbol", isSignal: true, isRequired: false, transformFunction: null }, drawable: { classPropertyName: "drawable", publicName: "drawable", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, selectedIcon: { classPropertyName: "selectedIcon", publicName: "selectedIcon", isSignal: true, isRequired: false, transformFunction: null }, systemItem: { classPropertyName: "systemItem", publicName: "systemItem", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, accessibilityLabel: { classPropertyName: "accessibilityLabel", publicName: "accessibilityLabel", isSignal: true, isRequired: false, transformFunction: null }, standardAppearance: { classPropertyName: "standardAppearance", publicName: "standardAppearance", isSignal: true, isRequired: false, transformFunction: null }, scrollEdgeAppearance: { classPropertyName: "scrollEdgeAppearance", publicName: "scrollEdgeAppearance", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "screenKey": "screenKey()", "title": "title()", "badgeValue": "badge()", "tabBarItemAccessibilityLabel": "accessibilityLabel()", "systemItem": "systemItem()", "isTitleUndefined": "title() === undefined", "iconType": "icons().iconType", "iconResourceName": "icons().iconResourceName", "iconImageSource": "icons().iconImageSource", "selectedIconResourceName": "icons().selectedIconResourceName", "selectedIconImageSource": "icons().selectedIconImageSource", "drawableIconResourceName": "icons().drawableIconResourceName", "imageIconResource": "icons().imageIconResource", "selectedDrawableIconResourceName": "icons().selectedDrawableIconResourceName", "selectedImageIconResource": "icons().selectedImageIconResource", "standardAppearance": "standard()", "scrollEdgeAppearance": "scrollEdge()" } }, ngImport: i0, template: '', isInline: true });
213
222
  }
214
223
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: NativeTab, decorators: [{
215
224
  type: Component,
@@ -217,7 +226,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
217
226
  selector: 'native-tab',
218
227
  template: '',
219
228
  host: {
220
- '[screenKey]': 'path()',
229
+ '[screenKey]': 'screenKey()',
221
230
  '[title]': 'title()',
222
231
  '[badgeValue]': 'badge()',
223
232
  '[tabBarItemAccessibilityLabel]': 'accessibilityLabel()',
@@ -1 +1 @@
1
- {"version":3,"file":"native-tab.js","sourceRoot":"","sources":["../src/native-tab.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One item in a native tab bar, and the screen behind it.\n *\n * Declared in the tabs outlet's template, the way a `<native-header>` is declared in a page's:\n *\n * ```html\n * <native-tabs-outlet>\n * <native-tab path=\"library\" title=\"Library\" sfSymbol=\"books.vertical.fill\" />\n * <native-tab path=\"inbox\" title=\"Inbox\" sfSymbol=\"tray\" [badge]=\"unread()\" />\n * </native-tabs-outlet>\n * ```\n *\n * `path` names the child route this tab selects, the same way a `routerLink` names one, and is\n * what native identifies the tab by. Everything else is an ordinary input, which is the point of\n * declaring tabs here rather than in the route config: a badge is a count that changes, a title\n * may be translated, and a tab may not exist at all until a feature flag says so. Route data\n * cannot bind to any of that.\n *\n * **A bar item is not a view.** `UITabBarItem` is a model object - a title, an image and a badge\n * string - and UIKit has no way to put a view in its place; Android's bottom-navigation item is\n * the same. That is why this takes no content, while `<native-header-item>` does: a navigation\n * bar really does hold subviews. What customisation exists is here as inputs: images instead of\n * symbols, a different icon when selected, and the per-state appearance below.\n *\n * The host element *is* the `RNSTabsScreen`. The outlet keeps the screen mounted and creates\n * each activated route's component on a child view, so destroying a route cannot remove this\n * tab item from the bar.\n */\nimport { Component, ElementRef, computed, inject, input, type OnInit } from '@angular/core';\nimport { nativePlatform, type EngineNode, HostEngine } from '@ng-native/fabric';\nimport { NATIVE_TAB_DEFAULTS } from './native-bar-defaults.ts';\nimport { ownHost } from './own-host.ts';\n\n/**\n * Where a tab's picture comes from.\n *\n * `template` is the one worth knowing about: the image is drawn as a mask and tinted to the bar's\n * colour, so it follows selection the way a symbol does. `image` renders as authored, which is\n * what a multi-colour logo wants and what a monochrome icon almost never does.\n */\nexport type TabIcon =\n | { readonly sfSymbol: string }\n /** An image in the iOS asset catalogue, by name. */\n | { readonly xcasset: string }\n /** A `require()`d asset or a `{ uri }`, drawn as authored. */\n | { readonly image: unknown }\n /** The same, drawn as a mask and tinted. */\n | { readonly template: unknown }\n /** Android: a drawable resource name. */\n | { readonly drawable: string };\n\n/** One of Apple's standard items, which brings its own icon and title. */\nexport type TabSystemItem =\n | 'none'\n | 'bookmarks'\n | 'contacts'\n | 'downloads'\n | 'favorites'\n | 'featured'\n | 'history'\n | 'more'\n | 'mostRecent'\n | 'mostViewed'\n | 'recents'\n | 'search'\n | 'topRated';\n\nexport type TabBarBlurEffect =\n | 'none'\n | 'systemDefault'\n | 'extraLight'\n | 'light'\n | 'dark'\n | 'regular'\n | 'prominent'\n | 'systemUltraThinMaterial'\n | 'systemThinMaterial'\n | 'systemMaterial'\n | 'systemThickMaterial'\n | 'systemChromeMaterial';\n\n/**\n * How an item looks in one state. The names are the native ones, spelled as the codegen spec\n * spells them, so this forwards rather than translates and Apple's own documentation still reads\n * across.\n */\nexport interface TabStateAppearance {\n tabBarItemTitleFontFamily?: string;\n tabBarItemTitleFontSize?: number;\n tabBarItemTitleFontWeight?: string | number;\n tabBarItemTitleFontStyle?: string;\n tabBarItemTitleFontColor?: string | number;\n tabBarItemTitlePositionAdjustment?: { horizontal?: number; vertical?: number };\n tabBarItemIconColor?: string | number;\n tabBarItemBadgeBackgroundColor?: string | number;\n}\n\n/** The four states an item can be in. Anything absent falls back to the platform's own. */\nexport interface TabItemAppearance {\n normal?: TabStateAppearance;\n selected?: TabStateAppearance;\n focused?: TabStateAppearance;\n disabled?: TabStateAppearance;\n}\n\n/**\n * How the bar looks, per layout. `stacked` is the usual icon-over-title item; `inline` and\n * `compactInline` are the side-by-side layouts iPad and a compact height use.\n */\nexport interface TabAppearance {\n stacked?: TabItemAppearance;\n inline?: TabItemAppearance;\n compactInline?: TabItemAppearance;\n tabBarBackgroundColor?: string | number;\n tabBarShadowColor?: string | number;\n tabBarBlurEffect?: TabBarBlurEffect;\n}\n\n/** One icon, as the props native reads it through. */\nexport interface TabIconProps {\n iconType?: string;\n iconResourceName?: string;\n iconImageSource?: unknown;\n drawableIconResourceName?: string;\n imageIconResource?: unknown;\n}\n\n/**\n * An icon and its selected counterpart, as one set of props.\n *\n * Native carries a single `iconType` for both states, so the two have to be the same kind of\n * thing: a template image cannot become a symbol when selected. React Native Screens throws on\n * that pairing and so does this, because the alternative - what happened before this checked - is\n * that native quietly keeps the unselected icon and nothing anywhere says why.\n */\nexport function tabIconProps(\n icon: TabIconProps,\n selectedIcon: TabIconProps,\n): TabIconProps & {\n selectedIconResourceName?: string;\n selectedIconImageSource?: unknown;\n selectedDrawableIconResourceName?: string;\n selectedImageIconResource?: unknown;\n} {\n const hasBase = icon.iconType !== undefined || icon.drawableIconResourceName !== undefined;\n const hasSelected =\n selectedIcon.iconType !== undefined || selectedIcon.drawableIconResourceName !== undefined;\n\n if (hasSelected && !hasBase) {\n throw new Error('[angular-native] <native-tab> has a selectedIcon but no icon to select from');\n }\n if (hasSelected && icon.iconType !== selectedIcon.iconType) {\n throw new Error(\n `[angular-native] <native-tab> icon and selectedIcon must be the same kind: native has ` +\n `one iconType for both, so '${icon.iconType}' and '${selectedIcon.iconType}' cannot pair.`,\n );\n }\n\n return {\n ...icon,\n selectedIconResourceName: selectedIcon.iconResourceName,\n selectedIconImageSource: selectedIcon.iconImageSource,\n selectedDrawableIconResourceName: selectedIcon.drawableIconResourceName,\n selectedImageIconResource: selectedIcon.imageIconResource,\n };\n}\n\ndeclare const ngDevMode: unknown;\n\n/** The paths already warned about, so a tab bar rebuilt by a navigation says it once. */\nconst warnedTabs = new Set<string>();\n\n/**\n * Says, once per path and in development only, that a tab has no icon on the platform running it\n * because it names only the other platform's shorthand. A tab written against the iOS simulator\n * with `sfSymbol` shows a title and nothing above it on Android.\n */\nfunction warnIfIconOnOnePlatform(tab: NativeTab): void {\n if (typeof ngDevMode !== 'undefined' && !ngDevMode) return;\n if (tab.icon() || tab.systemItem()) return;\n const [own, other, missing, platform] =\n nativePlatform() === 'android'\n ? [tab.drawable(), tab.sfSymbol(), 'drawable', 'Android']\n : nativePlatform() === 'ios'\n ? [tab.sfSymbol(), tab.drawable(), 'sfSymbol', 'iOS']\n : [];\n if (own || !other) return;\n const path = tab.path();\n if (warnedTabs.has(path)) return;\n warnedTabs.add(path);\n console.warn(\n `[angular-native] <native-tab path=\"${path}\"> has no icon on ${platform}: ${platform} reads ` +\n `only ${missing}. Add ${missing}=\"...\", or bind [icon] for a tab meant for one platform.`,\n );\n}\n\n@Component({\n selector: 'native-tab',\n template: '',\n host: {\n '[screenKey]': 'path()',\n '[title]': 'title()',\n '[badgeValue]': 'badge()',\n '[tabBarItemAccessibilityLabel]': 'accessibilityLabel()',\n '[systemItem]': 'systemItem()',\n // `isTitleUndefined` is what `TabsScreen.ios.tsx` derives from the title, and it is not a\n // synonym for \"no title\": with a title set and this left at its default, the item renders as\n // an icon with a gap where the label belongs.\n '[isTitleUndefined]': 'title() === undefined',\n '[iconType]': 'icons().iconType',\n '[iconResourceName]': 'icons().iconResourceName',\n '[iconImageSource]': 'icons().iconImageSource',\n '[selectedIconResourceName]': 'icons().selectedIconResourceName',\n '[selectedIconImageSource]': 'icons().selectedIconImageSource',\n '[drawableIconResourceName]': 'icons().drawableIconResourceName',\n '[imageIconResource]': 'icons().imageIconResource',\n '[selectedDrawableIconResourceName]': 'icons().selectedDrawableIconResourceName',\n '[selectedImageIconResource]': 'icons().selectedImageIconResource',\n '[standardAppearance]': 'standard()',\n '[scrollEdgeAppearance]': 'scrollEdge()',\n },\n})\nexport class NativeTab implements OnInit {\n private readonly engine = inject(HostEngine);\n /** The app's `withTabDefaults`, for an appearance this tab does not bind itself. */\n private readonly defaults = inject(NATIVE_TAB_DEFAULTS);\n\n /**\n * The `RNSTabsScreen` this tab is. The outlet creates the activated route's component inside\n * it, which is why it is public: a tab is a slot as well as a bar item.\n */\n readonly screen: EngineNode;\n\n constructor() {\n this.screen = ownHost(inject(ElementRef).nativeElement as EngineNode, this.constructor);\n }\n\n /**\n * The child route this tab selects, as a path segment: `library` for `/tabs/library`. Native\n * identifies the tab by it too, so it must be unique in the bar.\n */\n readonly path = input.required<string>();\n\n /** The label under the icon. Absent gives an icon-only item. */\n readonly title = input<string>();\n\n /** iOS: an SF Symbol name, e.g. `books.vertical.fill`. Shorthand for `[icon]`. */\n readonly sfSymbol = input<string>();\n\n /** Android: a drawable resource name. Shorthand for `[icon]`. */\n readonly drawable = input<string>();\n\n /** Anything a symbol name cannot say: an image, a template image, an asset-catalogue name. */\n readonly icon = input<TabIcon>();\n\n /** Shown while this tab is the selected one. Absent leaves the platform to tint `icon`. */\n readonly selectedIcon = input<TabIcon>();\n\n /** One of Apple's standard items, which supplies its own icon and title. */\n readonly systemItem = input<TabSystemItem>();\n\n /** The badge on the item: a count, or a single character for a dot. */\n readonly badge = input<string>();\n\n /** Announced instead of the title. */\n readonly accessibilityLabel = input<string>();\n\n /** How the item looks. Colours are processed here, because native never sees the object's keys. */\n readonly standardAppearance = input<TabAppearance>();\n\n /** The same, for when the scroll view behind the bar is at its edge. */\n readonly scrollEdgeAppearance = input<TabAppearance>();\n\n ngOnInit(): void {\n warnIfIconOnOnePlatform(this);\n }\n\n protected readonly standard = computed(() =>\n this.processAppearance(this.standardAppearance() ?? this.defaults().standardAppearance),\n );\n protected readonly scrollEdge = computed(() =>\n this.processAppearance(this.scrollEdgeAppearance() ?? this.defaults().scrollEdgeAppearance),\n );\n\n protected readonly icons = computed(() =>\n tabIconProps(\n this.iconProps(this.icon() ?? this.shorthand()),\n this.iconProps(this.selectedIcon()),\n ),\n );\n\n /**\n * `sfSymbol` and `drawable` say the same thing as `icon`, in the form most tabs want. A tab\n * names both to have an icon on both platforms, so the running platform's own comes first:\n * Android reads only a drawable, and iOS only a symbol.\n */\n private shorthand(): TabIcon | undefined {\n const symbol = this.sfSymbol();\n const drawable = this.drawable();\n const icons = [symbol && { sfSymbol: symbol }, drawable && { drawable }];\n if (nativePlatform() === 'android') icons.reverse();\n return icons.find((icon) => icon) || undefined;\n }\n\n /**\n * One icon, as the four props native reads it through. A `require()`d asset is an id until the\n * host resolves it, which is the same thing an `<image src>` needs and the engine only does for\n * props it knows by name.\n */\n private iconProps(icon: TabIcon | undefined): TabIconProps {\n if (!icon) return {};\n if ('sfSymbol' in icon) return { iconType: 'sfSymbol', iconResourceName: icon.sfSymbol };\n if ('xcasset' in icon) return { iconType: 'xcasset', iconResourceName: icon.xcasset };\n if ('drawable' in icon) return { drawableIconResourceName: icon.drawable };\n\n const source = this.engine.resolveAsset('image' in icon ? icon.image : icon.template);\n return nativePlatform() === 'android'\n ? { imageIconResource: source }\n : { iconType: 'image' in icon ? 'image' : 'template', iconImageSource: source };\n }\n\n private processAppearance(appearance: TabAppearance | undefined): object | undefined {\n if (!appearance) return undefined;\n return {\n ...appearance,\n stacked: this.processItem(appearance.stacked),\n inline: this.processItem(appearance.inline),\n compactInline: this.processItem(appearance.compactInline),\n tabBarBackgroundColor: this.engine.color(appearance.tabBarBackgroundColor),\n tabBarShadowColor: this.engine.color(appearance.tabBarShadowColor),\n };\n }\n\n private processItem(item: TabItemAppearance | undefined): object | undefined {\n if (!item) return undefined;\n return {\n normal: this.processState(item.normal),\n selected: this.processState(item.selected),\n focused: this.processState(item.focused),\n disabled: this.processState(item.disabled),\n };\n }\n\n private processState(state: TabStateAppearance | undefined): object | undefined {\n if (!state) return undefined;\n return {\n ...state,\n // Native declares the weight as a string, and `600` is the natural way to write one.\n tabBarItemTitleFontWeight:\n state.tabBarItemTitleFontWeight === undefined\n ? undefined\n : String(state.tabBarItemTitleFontWeight),\n tabBarItemTitleFontColor: this.engine.color(state.tabBarItemTitleFontColor),\n tabBarItemIconColor: this.engine.color(state.tabBarItemIconColor),\n tabBarItemBadgeBackgroundColor: this.engine.color(state.tabBarItemBadgeBackgroundColor),\n };\n }\n}\n"]}
1
+ {"version":3,"file":"native-tab.js","sourceRoot":"","sources":["../src/native-tab.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One item in a native tab bar, and the screen behind it.\n *\n * Declared in the tabs outlet's template, the way a `<native-header>` is declared in a page's:\n *\n * ```html\n * <native-tabs-outlet>\n * <native-tab path=\"library\" title=\"Library\" sfSymbol=\"books.vertical.fill\" />\n * <native-tab path=\"inbox\" title=\"Inbox\" sfSymbol=\"tray\" [badge]=\"unread()\" />\n * </native-tabs-outlet>\n * ```\n *\n * `path` names the child route this tab selects, the same way a `routerLink` names one, and is\n * what native identifies the tab by. Everything else is an ordinary input, which is the point of\n * declaring tabs here rather than in the route config: a badge is a count that changes, a title\n * may be translated, and a tab may not exist at all until a feature flag says so. Route data\n * cannot bind to any of that.\n *\n * **A bar item is not a view.** `UITabBarItem` is a model object - a title, an image and a badge\n * string - and UIKit has no way to put a view in its place; Android's bottom-navigation item is\n * the same. That is why this takes no content, while `<native-header-item>` does: a navigation\n * bar really does hold subviews. What customisation exists is here as inputs: images instead of\n * symbols, a different icon when selected, and the per-state appearance below.\n *\n * The host element *is* the `RNSTabsScreen`. The outlet keeps the screen mounted and creates\n * each activated route's component on a child view, so destroying a route cannot remove this\n * tab item from the bar.\n */\nimport { Component, ElementRef, computed, inject, input, type OnInit } from '@angular/core';\nimport { nativePlatform, type EngineNode, HostEngine } from '@ng-native/fabric';\nimport { NATIVE_TAB_DEFAULTS } from './native-bar-defaults.ts';\nimport { ownHost } from './own-host.ts';\n\n/**\n * Where a tab's picture comes from.\n *\n * `template` is the one worth knowing about: the image is drawn as a mask and tinted to the bar's\n * colour, so it follows selection the way a symbol does. `image` renders as authored, which is\n * what a multi-colour logo wants and what a monochrome icon almost never does.\n */\nexport type TabIcon =\n | { readonly sfSymbol: string }\n /** An image in the iOS asset catalogue, by name. */\n | { readonly xcasset: string }\n /** A `require()`d asset or a `{ uri }`, drawn as authored. */\n | { readonly image: unknown }\n /** The same, drawn as a mask and tinted. */\n | { readonly template: unknown }\n /** Android: a drawable resource name. */\n | { readonly drawable: string };\n\n/** One of Apple's standard items, which brings its own icon and title. */\nexport type TabSystemItem =\n | 'none'\n | 'bookmarks'\n | 'contacts'\n | 'downloads'\n | 'favorites'\n | 'featured'\n | 'history'\n | 'more'\n | 'mostRecent'\n | 'mostViewed'\n | 'recents'\n | 'search'\n | 'topRated';\n\nexport type TabBarBlurEffect =\n | 'none'\n | 'systemDefault'\n | 'extraLight'\n | 'light'\n | 'dark'\n | 'regular'\n | 'prominent'\n | 'systemUltraThinMaterial'\n | 'systemThinMaterial'\n | 'systemMaterial'\n | 'systemThickMaterial'\n | 'systemChromeMaterial';\n\n/**\n * How an item looks in one state. The names are the native ones, spelled as the codegen spec\n * spells them, so this forwards rather than translates and Apple's own documentation still reads\n * across.\n */\nexport interface TabStateAppearance {\n tabBarItemTitleFontFamily?: string;\n tabBarItemTitleFontSize?: number;\n tabBarItemTitleFontWeight?: string | number;\n tabBarItemTitleFontStyle?: string;\n tabBarItemTitleFontColor?: string | number;\n tabBarItemTitlePositionAdjustment?: { horizontal?: number; vertical?: number };\n tabBarItemIconColor?: string | number;\n tabBarItemBadgeBackgroundColor?: string | number;\n}\n\n/** The four states an item can be in. Anything absent falls back to the platform's own. */\nexport interface TabItemAppearance {\n normal?: TabStateAppearance;\n selected?: TabStateAppearance;\n focused?: TabStateAppearance;\n disabled?: TabStateAppearance;\n}\n\n/**\n * How the bar looks, per layout. `stacked` is the usual icon-over-title item; `inline` and\n * `compactInline` are the side-by-side layouts iPad and a compact height use.\n */\nexport interface TabAppearance {\n stacked?: TabItemAppearance;\n inline?: TabItemAppearance;\n compactInline?: TabItemAppearance;\n tabBarBackgroundColor?: string | number;\n tabBarShadowColor?: string | number;\n tabBarBlurEffect?: TabBarBlurEffect;\n}\n\n/** One icon, as the props native reads it through. */\nexport interface TabIconProps {\n iconType?: string;\n iconResourceName?: string;\n iconImageSource?: unknown;\n drawableIconResourceName?: string;\n imageIconResource?: unknown;\n}\n\n/**\n * An icon and its selected counterpart, as one set of props.\n *\n * Native carries a single `iconType` for both states, so the two have to be the same kind of\n * thing: a template image cannot become a symbol when selected. React Native Screens throws on\n * that pairing and so does this, because the alternative - what happened before this checked - is\n * that native quietly keeps the unselected icon and nothing anywhere says why.\n */\nexport function tabIconProps(\n icon: TabIconProps,\n selectedIcon: TabIconProps,\n): TabIconProps & {\n selectedIconResourceName?: string;\n selectedIconImageSource?: unknown;\n selectedDrawableIconResourceName?: string;\n selectedImageIconResource?: unknown;\n} {\n const hasBase = icon.iconType !== undefined || icon.drawableIconResourceName !== undefined;\n const hasSelected =\n selectedIcon.iconType !== undefined || selectedIcon.drawableIconResourceName !== undefined;\n\n if (hasSelected && !hasBase) {\n throw new Error('[angular-native] <native-tab> has a selectedIcon but no icon to select from');\n }\n if (hasSelected && icon.iconType !== selectedIcon.iconType) {\n throw new Error(\n `[angular-native] <native-tab> icon and selectedIcon must be the same kind: native has ` +\n `one iconType for both, so '${icon.iconType}' and '${selectedIcon.iconType}' cannot pair.`,\n );\n }\n\n return {\n ...icon,\n selectedIconResourceName: selectedIcon.iconResourceName,\n selectedIconImageSource: selectedIcon.iconImageSource,\n selectedDrawableIconResourceName: selectedIcon.drawableIconResourceName,\n selectedImageIconResource: selectedIcon.imageIconResource,\n };\n}\n\ndeclare const ngDevMode: unknown;\n\n/** The paths already warned about, so a tab bar rebuilt by a navigation says it once. */\nconst warnedTabs = new Set<string>();\n\n/**\n * Says, once per path and in development only, that a tab has no icon on the platform running it\n * because it names only the other platform's shorthand. A tab written against the iOS simulator\n * with `sfSymbol` shows a title and nothing above it on Android.\n */\nfunction warnIfIconOnOnePlatform(tab: NativeTab): void {\n if (typeof ngDevMode !== 'undefined' && !ngDevMode) return;\n if (tab.icon() || tab.systemItem()) return;\n const [own, other, missing, platform] =\n nativePlatform() === 'android'\n ? [tab.drawable(), tab.sfSymbol(), 'drawable', 'Android']\n : nativePlatform() === 'ios'\n ? [tab.sfSymbol(), tab.drawable(), 'sfSymbol', 'iOS']\n : [];\n if (own || !other) return;\n const path = tab.path();\n if (warnedTabs.has(path)) return;\n warnedTabs.add(path);\n console.warn(\n `[angular-native] <native-tab path=\"${path}\"> has no icon on ${platform}: ${platform} reads ` +\n `only ${missing}. Add ${missing}=\"...\", or bind [icon] for a tab meant for one platform.`,\n );\n}\n\n/**\n * The key native identifies a tab by: its path, or `/` for the tab at path `''`, since iOS reads an\n * empty key as none and fails on the tap that selects it. No route path starts with `/`.\n */\nexport function screenKeyOf(path: string): string {\n return path || '/';\n}\n\n@Component({\n selector: 'native-tab',\n template: '',\n host: {\n '[screenKey]': 'screenKey()',\n '[title]': 'title()',\n '[badgeValue]': 'badge()',\n '[tabBarItemAccessibilityLabel]': 'accessibilityLabel()',\n '[systemItem]': 'systemItem()',\n // `isTitleUndefined` is what `TabsScreen.ios.tsx` derives from the title, and it is not a\n // synonym for \"no title\": with a title set and this left at its default, the item renders as\n // an icon with a gap where the label belongs.\n '[isTitleUndefined]': 'title() === undefined',\n '[iconType]': 'icons().iconType',\n '[iconResourceName]': 'icons().iconResourceName',\n '[iconImageSource]': 'icons().iconImageSource',\n '[selectedIconResourceName]': 'icons().selectedIconResourceName',\n '[selectedIconImageSource]': 'icons().selectedIconImageSource',\n '[drawableIconResourceName]': 'icons().drawableIconResourceName',\n '[imageIconResource]': 'icons().imageIconResource',\n '[selectedDrawableIconResourceName]': 'icons().selectedDrawableIconResourceName',\n '[selectedImageIconResource]': 'icons().selectedImageIconResource',\n '[standardAppearance]': 'standard()',\n '[scrollEdgeAppearance]': 'scrollEdge()',\n },\n})\nexport class NativeTab implements OnInit {\n private readonly engine = inject(HostEngine);\n /** The app's `withTabDefaults`, for an appearance this tab does not bind itself. */\n private readonly defaults = inject(NATIVE_TAB_DEFAULTS);\n\n /**\n * The `RNSTabsScreen` this tab is. The outlet creates the activated route's component inside\n * it, which is why it is public: a tab is a slot as well as a bar item.\n */\n readonly screen: EngineNode;\n\n constructor() {\n this.screen = ownHost(inject(ElementRef).nativeElement as EngineNode, this.constructor);\n }\n\n /**\n * The child route this tab selects, as a path segment: `library` for `/tabs/library`. Native\n * identifies the tab by it too, so it must be unique in the bar.\n */\n readonly path = input.required<string>();\n\n protected readonly screenKey = computed(() => screenKeyOf(this.path()));\n\n /** The label under the icon. Absent gives an icon-only item. */\n readonly title = input<string>();\n\n /** iOS: an SF Symbol name, e.g. `books.vertical.fill`. Shorthand for `[icon]`. */\n readonly sfSymbol = input<string>();\n\n /** Android: a drawable resource name. Shorthand for `[icon]`. */\n readonly drawable = input<string>();\n\n /** Anything a symbol name cannot say: an image, a template image, an asset-catalogue name. */\n readonly icon = input<TabIcon>();\n\n /** Shown while this tab is the selected one. Absent leaves the platform to tint `icon`. */\n readonly selectedIcon = input<TabIcon>();\n\n /** One of Apple's standard items, which supplies its own icon and title. */\n readonly systemItem = input<TabSystemItem>();\n\n /** The badge on the item: a count, or a single character for a dot. */\n readonly badge = input<string>();\n\n /** Announced instead of the title. */\n readonly accessibilityLabel = input<string>();\n\n /** How the item looks. Colours are processed here, because native never sees the object's keys. */\n readonly standardAppearance = input<TabAppearance>();\n\n /** The same, for when the scroll view behind the bar is at its edge. */\n readonly scrollEdgeAppearance = input<TabAppearance>();\n\n ngOnInit(): void {\n warnIfIconOnOnePlatform(this);\n }\n\n protected readonly standard = computed(() =>\n this.processAppearance(this.standardAppearance() ?? this.defaults().standardAppearance),\n );\n protected readonly scrollEdge = computed(() =>\n this.processAppearance(this.scrollEdgeAppearance() ?? this.defaults().scrollEdgeAppearance),\n );\n\n protected readonly icons = computed(() =>\n tabIconProps(\n this.iconProps(this.icon() ?? this.shorthand()),\n this.iconProps(this.selectedIcon()),\n ),\n );\n\n /**\n * `sfSymbol` and `drawable` say the same thing as `icon`, in the form most tabs want. A tab\n * names both to have an icon on both platforms, so the running platform's own comes first:\n * Android reads only a drawable, and iOS only a symbol.\n */\n private shorthand(): TabIcon | undefined {\n const symbol = this.sfSymbol();\n const drawable = this.drawable();\n const icons = [symbol && { sfSymbol: symbol }, drawable && { drawable }];\n if (nativePlatform() === 'android') icons.reverse();\n return icons.find((icon) => icon) || undefined;\n }\n\n /**\n * One icon, as the four props native reads it through. A `require()`d asset is an id until the\n * host resolves it, which is the same thing an `<image src>` needs and the engine only does for\n * props it knows by name.\n */\n private iconProps(icon: TabIcon | undefined): TabIconProps {\n if (!icon) return {};\n if ('sfSymbol' in icon) return { iconType: 'sfSymbol', iconResourceName: icon.sfSymbol };\n if ('xcasset' in icon) return { iconType: 'xcasset', iconResourceName: icon.xcasset };\n if ('drawable' in icon) return { drawableIconResourceName: icon.drawable };\n\n const source = this.engine.resolveAsset('image' in icon ? icon.image : icon.template);\n return nativePlatform() === 'android'\n ? { imageIconResource: source }\n : { iconType: 'image' in icon ? 'image' : 'template', iconImageSource: source };\n }\n\n private processAppearance(appearance: TabAppearance | undefined): object | undefined {\n if (!appearance) return undefined;\n return {\n ...appearance,\n stacked: this.processItem(appearance.stacked),\n inline: this.processItem(appearance.inline),\n compactInline: this.processItem(appearance.compactInline),\n tabBarBackgroundColor: this.engine.color(appearance.tabBarBackgroundColor),\n tabBarShadowColor: this.engine.color(appearance.tabBarShadowColor),\n };\n }\n\n private processItem(item: TabItemAppearance | undefined): object | undefined {\n if (!item) return undefined;\n return {\n normal: this.processState(item.normal),\n selected: this.processState(item.selected),\n focused: this.processState(item.focused),\n disabled: this.processState(item.disabled),\n };\n }\n\n private processState(state: TabStateAppearance | undefined): object | undefined {\n if (!state) return undefined;\n return {\n ...state,\n // Native declares the weight as a string, and `600` is the natural way to write one.\n tabBarItemTitleFontWeight:\n state.tabBarItemTitleFontWeight === undefined\n ? undefined\n : String(state.tabBarItemTitleFontWeight),\n tabBarItemTitleFontColor: this.engine.color(state.tabBarItemTitleFontColor),\n tabBarItemIconColor: this.engine.color(state.tabBarItemIconColor),\n tabBarItemBadgeBackgroundColor: this.engine.color(state.tabBarItemBadgeBackgroundColor),\n };\n }\n}\n"]}
@@ -87,6 +87,12 @@ export declare class NativeTabsOutlet implements RouterOutletContract, AfterCont
87
87
  /** The input binder reads this to bind route params to component inputs. */
88
88
  get activatedComponentRef(): ComponentRef<unknown> | null;
89
89
  activateWith(route: ActivatedRoute, environmentInjector: EnvironmentInjector): void;
90
+ /**
91
+ * Destroy the tab page in front: nothing once the router has detached it, as Angular's own
92
+ * outlet does nothing then. A tab under a route with no component of its own, a `loadChildren`
93
+ * wrapper or a group, has the router detach the page and then deactivate this same outlet, and
94
+ * that page is the tab, kept for when it is selected again.
95
+ */
90
96
  deactivate(): void;
91
97
  /**
92
98
  * Hand the live `ComponentRef` back to the router. The tab's screen stays where it is with the
@@ -109,9 +115,19 @@ export declare class NativeTabsOutlet implements RouterOutletContract, AfterCont
109
115
  * exist, which is all this needs.
110
116
  */
111
117
  private readTabs;
118
+ /**
119
+ * The tab `route` is the page of, by its path from this outlet's route down to it. A route with
120
+ * no component above the page is part of that path: a `loadChildren` wrapper takes the tab's
121
+ * path and leaves `''` to the page under it, and a group is a `''` of its own.
122
+ */
112
123
  private entryFor;
113
124
  /** Remember where this tab is, and tell native to show it. */
114
125
  private select;
126
+ /**
127
+ * Another tab came in front of `entry`: its stacks dismiss what they presented, and it is
128
+ * remembered at the page beneath, where the outermost stack that dismissed something is left.
129
+ */
130
+ private leave;
115
131
  /**
116
132
  * A back with nothing left to pop inside the tab: go to the first tab, where it was left, which
117
133
  * is what a bottom navigation bar does on Android. From the first tab, decline, so the platform
@@ -129,6 +145,15 @@ export declare class NativeTabsOutlet implements RouterOutletContract, AfterCont
129
145
  private unopenedTabOf;
130
146
  /** Whether `url` is a tab other than the one in front, or a page inside one. */
131
147
  private behind;
148
+ /**
149
+ * The tab `url` is in: the one with the longest url that starts it. A tab at `''` has the bar's
150
+ * own url, which at the root starts every url, pages outside the bar included, so it takes only
151
+ * a url its routes lead to, as far as they are loaded. A wildcard or a custom matcher is not
152
+ * followed, as `taken` follows neither: a page only one of them shows is in no tab.
153
+ */
154
+ private tabOf;
155
+ /** Whether the routes of the tab at `''` lead to `rest`, the path under the bar's own url. */
156
+ private leadsInto;
132
157
  private request;
133
158
  /**
134
159
  * Native switched tab. Acknowledge the new state, then navigate: the router is authoritative
@@ -145,7 +170,10 @@ export declare class NativeTabsOutlet implements RouterOutletContract, AfterCont
145
170
  private revertUnless;
146
171
  /** A tab's own url, from the outlet's route down. */
147
172
  private pathOf;
148
- /** As the stack outlet's: the route, the outlet contexts, and whether the tab is showing. */
173
+ /**
174
+ * As the stack outlet's: the route, the outlet contexts, and whether the tab is showing. And the
175
+ * tab itself, for its stacks to hear when another comes in front.
176
+ */
149
177
  private outletInjector;
150
178
  static ɵfac: i0.ɵɵFactoryDeclaration<NativeTabsOutlet, never>;
151
179
  static ɵcmp: i0.ɵɵComponentDeclaration<NativeTabsOutlet, "native-tabs-outlet", never, { "barHidden": { "alias": "barHidden"; "required": false; "isSignal": true; }; "tintColor": { "alias": "tintColor"; "required": false; "isSignal": true; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; "isSignal": true; }; "colorScheme": { "alias": "colorScheme"; "required": false; "isSignal": true; }; "minimizeBehavior": { "alias": "minimizeBehavior"; "required": false; "isSignal": true; }; "controllerMode": { "alias": "controllerMode"; "required": false; "isSignal": true; }; "respectsKeyboard": { "alias": "respectsKeyboard"; "required": false; "isSignal": true; }; }, {}, ["declared"], ["*"], true, never>;
@@ -32,11 +32,33 @@ import { bindRouteInputs } from "./bind-route-inputs.js";
32
32
  import { NATIVE_TAB_DEFAULTS } from "./native-bar-defaults.js";
33
33
  import { NativeBack, isShowing } from "./native-back.js";
34
34
  import { optionalBoolean } from "./transforms.js";
35
- import { NativeTab } from "./native-tab.js";
35
+ import { NativeTab, screenKeyOf } from "./native-tab.js";
36
36
  import { ownHost } from "./own-host.js";
37
37
  import { markTabRoute } from "./tab-routes.js";
38
- import { withoutPresented } from "./presented-route.js";
38
+ import { IN_TAB } from "./in-tab.js";
39
+ import { taken, withoutPresented } from "./presented-route.js";
39
40
  import * as i0 from "@angular/core";
41
+ /** A url without its query or fragment. */
42
+ const pathOnly = (url) => url.split(/[?#]/, 1)[0];
43
+ /**
44
+ * Whether one of `routes` matches `segments`, whole or up to lazy children not loaded yet, which a
45
+ * route that took a static segment of its own is taken to lead to. A parameter alone is not: it
46
+ * takes any segment, pages outside the bar included, and its children may match none of the rest.
47
+ */
48
+ function reaches(routes, segments) {
49
+ return routes.some((route) => {
50
+ const count = taken(route, segments);
51
+ if (count === null)
52
+ return false;
53
+ const rest = segments.slice(count);
54
+ if (rest.length === 0)
55
+ return true;
56
+ if (route.children)
57
+ return reaches(route.children, rest);
58
+ const named = route.path.split('/').some((part) => part && !part.startsWith(':'));
59
+ return named && route.loadChildren !== undefined;
60
+ });
61
+ }
40
62
  export class NativeTabsOutlet {
41
63
  name = PRIMARY_OUTLET;
42
64
  supportsBindingToComponentInputs = true;
@@ -178,7 +200,7 @@ export class NativeTabsOutlet {
178
200
  try {
179
201
  ref = createComponent(component, {
180
202
  environmentInjector,
181
- elementInjector: this.outletInjector(route, entry.inFront),
203
+ elementInjector: this.outletInjector(route, entry),
182
204
  hostElement: content,
183
205
  });
184
206
  this.applicationRef.attachView(ref.hostView);
@@ -205,10 +227,16 @@ export class NativeTabsOutlet {
205
227
  entry.detached = false;
206
228
  this.select(entry);
207
229
  }
230
+ /**
231
+ * Destroy the tab page in front: nothing once the router has detached it, as Angular's own
232
+ * outlet does nothing then. A tab under a route with no component of its own, a `loadChildren`
233
+ * wrapper or a group, has the router detach the page and then deactivate this same outlet, and
234
+ * that page is the tab, kept for when it is selected again.
235
+ */
208
236
  deactivate() {
209
- const entry = this.selected;
210
- if (!entry?.ref)
237
+ if (!this.isActivated)
211
238
  return;
239
+ const entry = this.selected;
212
240
  entry.unbind();
213
241
  entry.unbind = () => { };
214
242
  entry.ref.destroy();
@@ -288,6 +316,7 @@ export class NativeTabsOutlet {
288
316
  detached: false,
289
317
  unbind: () => { },
290
318
  inFront: signal(false),
319
+ leaving: new Set(),
291
320
  });
292
321
  // The reuse strategy detaches a tab whole, and has only the route to go on.
293
322
  // A path can have multiple route configs selected by canMatch (for example an iPad split
@@ -306,23 +335,53 @@ export class NativeTabsOutlet {
306
335
  this.request(this.entries[0]);
307
336
  return this.entries;
308
337
  }
338
+ /**
339
+ * The tab `route` is the page of, by its path from this outlet's route down to it. A route with
340
+ * no component above the page is part of that path: a `loadChildren` wrapper takes the tab's
341
+ * path and leaves `''` to the page under it, and a group is a `''` of its own.
342
+ */
309
343
  entryFor(route) {
310
- const path = (route.routeConfig ?? route.snapshot.routeConfig)?.path;
344
+ const config = route.routeConfig ?? route.snapshot.routeConfig;
345
+ const paths = [config?.path];
346
+ for (let above = route.parent; above && !above.component; above = above.parent) {
347
+ paths.unshift(above.routeConfig?.path);
348
+ }
349
+ const path = paths.filter(Boolean).join('/');
311
350
  const entry = this.readTabs().find((candidate) => candidate.key === path);
312
351
  if (!entry) {
313
352
  throw new Error(`[angular-native] no <native-tab> has path="${path}". Every child route the bar can ` +
314
353
  `reach needs one, and its path is what native identifies the tab by.`);
315
354
  }
355
+ // Under a wrapper the page's config is not one of the children `readTabs` marked, and it is
356
+ // the one the reuse strategy is asked to detach whole.
357
+ if (config)
358
+ markTabRoute(config);
316
359
  return entry;
317
360
  }
318
361
  /** Remember where this tab is, and tell native to show it. */
319
362
  select(entry) {
363
+ const left = this.selected;
364
+ if (left && left !== entry)
365
+ this.leave(left);
320
366
  this.selected = entry;
321
367
  for (const other of this.entries)
322
368
  other.inFront.set(other === entry);
323
369
  entry.url = this.router?.url ?? entry.url;
324
370
  this.request(entry);
325
371
  }
372
+ /**
373
+ * Another tab came in front of `entry`: its stacks dismiss what they presented, and it is
374
+ * remembered at the page beneath, where the outermost stack that dismissed something is left.
375
+ */
376
+ leave(entry) {
377
+ let url = null;
378
+ for (const dismiss of entry.leaving) {
379
+ const beneath = dismiss();
380
+ url ??= beneath;
381
+ }
382
+ if (url !== null)
383
+ entry.url = url;
384
+ }
326
385
  /**
327
386
  * A back with nothing left to pop inside the tab: go to the first tab, where it was left, which
328
387
  * is what a bottom navigation bar does on Android. From the first tab, decline, so the platform
@@ -343,10 +402,7 @@ export class NativeTabsOutlet {
343
402
  /** Keep the tab in front on the url it has reached, as long as the url is still inside it. */
344
403
  follow(url) {
345
404
  const entry = this.selected;
346
- if (!entry)
347
- return;
348
- const root = this.pathOf(entry);
349
- if (url === root || url.startsWith(`${root}/`) || url.startsWith(`${root}?`))
405
+ if (entry && this.tabOf(url) === entry)
350
406
  entry.url = url;
351
407
  }
352
408
  /**
@@ -354,27 +410,49 @@ export class NativeTabsOutlet {
354
410
  * first screen, which a push into the tab from outside it shows first.
355
411
  */
356
412
  unopenedTabOf(url) {
357
- for (const entry of this.entries) {
358
- if (entry.ref)
359
- continue;
360
- const root = this.pathOf(entry);
361
- if (url.startsWith(`${root}/`))
362
- return root;
363
- }
364
- return null;
413
+ const entry = this.tabOf(url);
414
+ if (!entry || entry.ref)
415
+ return null;
416
+ const root = this.pathOf(entry);
417
+ return pathOnly(url) === root ? null : root;
365
418
  }
366
419
  /** Whether `url` is a tab other than the one in front, or a page inside one. */
367
420
  behind(url) {
368
- return this.entries.some((entry) => {
369
- if (entry === this.selected)
370
- return false;
421
+ const entry = this.tabOf(url);
422
+ return entry !== null && entry !== this.selected;
423
+ }
424
+ /**
425
+ * The tab `url` is in: the one with the longest url that starts it. A tab at `''` has the bar's
426
+ * own url, which at the root starts every url, pages outside the bar included, so it takes only
427
+ * a url its routes lead to, as far as they are loaded. A wildcard or a custom matcher is not
428
+ * followed, as `taken` follows neither: a page only one of them shows is in no tab.
429
+ */
430
+ tabOf(url) {
431
+ const path = pathOnly(url);
432
+ let found = null;
433
+ for (const entry of this.entries) {
371
434
  const root = this.pathOf(entry);
372
- return url === root || url.startsWith(`${root}/`);
373
- });
435
+ const inside = path === root || path.startsWith(root.endsWith('/') ? root : `${root}/`);
436
+ if (!inside || (found && this.pathOf(found).length >= root.length))
437
+ continue;
438
+ if (!entry.key && !this.leadsInto(entry, path.slice(root.length)))
439
+ continue;
440
+ found = entry;
441
+ }
442
+ return found;
443
+ }
444
+ /** Whether the routes of the tab at `''` lead to `rest`, the path under the bar's own url. */
445
+ leadsInto(entry, rest) {
446
+ const segments = rest.split('/').filter(Boolean);
447
+ const own = entry.route?.snapshot.routeConfig;
448
+ const routes = own
449
+ ? [own]
450
+ : (this.route.routeConfig?.children ?? []).filter((config) => config.path === '');
451
+ return segments.length === 0 || reaches(routes, segments);
374
452
  }
375
453
  request(entry) {
376
454
  this.renderer.setProperty(this.host.nativeElement, 'navStateRequest', {
377
- selectedScreenKey: entry.key,
455
+ selectedScreenKey: screenKeyOf(entry.key),
378
456
  baseProvenance: this.provenance,
379
457
  });
380
458
  }
@@ -387,7 +465,7 @@ export class NativeTabsOutlet {
387
465
  if (!selection)
388
466
  return;
389
467
  this.provenance = selection.provenance;
390
- const entry = this.readTabs().find((tab) => tab.key === selection.selectedScreenKey);
468
+ const entry = this.readTabs().find((tab) => screenKeyOf(tab.key) === selection.selectedScreenKey);
391
469
  if (!entry || entry === this.selected)
392
470
  return;
393
471
  // Returning to a tab returns to the url it was last on, which is the whole point of keeping
@@ -423,13 +501,20 @@ export class NativeTabsOutlet {
423
501
  .join('/');
424
502
  return `/${[parent, entry.key].filter(Boolean).join('/')}`;
425
503
  }
426
- /** As the stack outlet's: the route, the outlet contexts, and whether the tab is showing. */
427
- outletInjector(route, inFront) {
504
+ /**
505
+ * As the stack outlet's: the route, the outlet contexts, and whether the tab is showing. And the
506
+ * tab itself, for its stacks to hear when another comes in front.
507
+ */
508
+ outletInjector(route, entry) {
428
509
  const outer = this.injector.get(SCREEN_IN_FRONT);
510
+ const tab = {
511
+ onLeave: (leave) => (entry.leaving.add(leave), () => entry.leaving.delete(leave)),
512
+ };
429
513
  return Injector.create({
430
514
  parent: this.injector,
431
515
  providers: [
432
- { provide: SCREEN_IN_FRONT, useValue: computed(() => outer() && inFront()) },
516
+ { provide: SCREEN_IN_FRONT, useValue: computed(() => outer() && entry.inFront()) },
517
+ { provide: IN_TAB, useValue: tab },
433
518
  { provide: ActivatedRoute, useValue: route },
434
519
  {
435
520
  provide: ChildrenOutletContexts,