diff --git a/goldens/public-api/router/index.api.md b/goldens/public-api/router/index.api.md index 6c089276f6e..a06ec2f0f3a 100644 --- a/goldens/public-api/router/index.api.md +++ b/goldens/public-api/router/index.api.md @@ -801,7 +801,7 @@ export interface RouterFeature { } // @public -export type RouterFeatures = PreloadingFeature | DebugTracingFeature | InitialNavigationFeature | InMemoryScrollingFeature | RouterConfigurationFeature | NavigationErrorHandlerFeature | ComponentInputBindingFeature | ViewTransitionsFeature | ExperimentalAutoCleanupInjectorsFeature | RouterHashLocationFeature; +export type RouterFeatures = PreloadingFeature | DebugTracingFeature | InitialNavigationFeature | InMemoryScrollingFeature | RouterConfigurationFeature | NavigationErrorHandlerFeature | ComponentInputBindingFeature | ViewTransitionsFeature | ExperimentalAutoCleanupInjectorsFeature | RouterHashLocationFeature | ExperimentalPlatformNavigationFeature; // @public export type RouterHashLocationFeature = RouterFeature; @@ -1144,6 +1144,9 @@ export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNa // @public export function withExperimentalAutoCleanupInjectors(): ExperimentalAutoCleanupInjectorsFeature; +// @public +export function withExperimentalPlatformNavigation(): ExperimentalPlatformNavigationFeature; + // @public export function withHashLocation(): RouterHashLocationFeature; diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 6b2ef229c2c..ba97079dbd6 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -82,6 +82,7 @@ export { NavigationErrorHandlerFeature, PreloadingFeature, provideRouter, + withExperimentalPlatformNavigation, provideRoutes, RouterConfigurationFeature, RouterFeature, diff --git a/packages/router/src/private_export.ts b/packages/router/src/private_export.ts index e5106c0006b..b508c826258 100644 --- a/packages/router/src/private_export.ts +++ b/packages/router/src/private_export.ts @@ -11,4 +11,3 @@ export {RestoredState as ɵRestoredState} from './navigation_transition'; export {loadChildren as ɵloadChildren} from './router_config_loader'; export {ROUTER_PROVIDERS as ɵROUTER_PROVIDERS} from './router_module'; export {afterNextNavigation as ɵafterNextNavigation} from './utils/navigations'; -export {withPlatformNavigation as ɵwithPlatformNavigation} from './provide_router'; diff --git a/packages/router/src/provide_router.ts b/packages/router/src/provide_router.ts index 7db0d0ae08c..82e00d87bb6 100644 --- a/packages/router/src/provide_router.ts +++ b/packages/router/src/provide_router.ts @@ -237,13 +237,36 @@ export function withInMemoryScrolling( } /** - * Enables the use of the browser's `History` API for navigation. + * A type alias for providers returned by `withExperimentalPlatformNavigation` for use with `provideRouter`. + * + * @see {@link withExperimentalPlatformNavigation} + * @see {@link provideRouter} + * + * @experimental 21.1 + */ +export type ExperimentalPlatformNavigationFeature = + RouterFeature; + +/** + * Enables interop with the browser's `Navigation` API for router navigations. * * @description - * This function provides a `Location` strategy that uses the browser's `History` API. - * It is required when using features that rely on `history.state`. For example, the - * `state` object in `NavigationExtras` is passed to `history.pushState` or - * `history.replaceState`. + * + * CRITICAL: This feature is _highly_ experimental and should not be used in production. Browser support + * is limited and in active development. Use only for experimentation and feedback purposes. + * + * This function provides a `Location` strategy that uses the browser's `Navigation` API. + * By using the platform's Navigation APIs, the Router is able to provide native + * browser navigation capabilities. Some advantages include: + * + * - The ability to intercept navigations triggered outside the Router. This allows plain anchor + * elements _without_ `RouterLink` directives to be intercepted by the Router and converted to SPA navigations. + * - Native scroll and focus restoration support by the browser, without the need for custom implementations. + * - Communication of ongoing navigations to the browser, enabling built-in features like + * accessibility announcements, loading indicators, stop buttons, and performance measurement APIs. + + * NOTE: Deferred entry updates are not part of the interop 2025 Navigation API commitments so the "ongoing navigation" + * communication support is limited. * * @usageNotes * @@ -254,14 +277,19 @@ export function withInMemoryScrolling( * * bootstrapApplication(AppComponent, { * providers: [ - * provideRouter(appRoutes, withPlatformNavigation()) + * provideRouter(appRoutes, withExperimentalPlatformNavigation()) * ] * }); * ``` + * + * @see https://github.com/WICG/navigation-api?tab=readme-ov-file#problem-statement + * @see https://developer.chrome.com/docs/web-platform/navigation-api/ + * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API * + * @experimental 21.1 * @returns A `RouterFeature` that enables the platform navigation. */ -export function withPlatformNavigation() { +export function withExperimentalPlatformNavigation(): ExperimentalPlatformNavigationFeature { const devModeLocationCheck = typeof ngDevMode === 'undefined' || ngDevMode ? [ @@ -270,10 +298,10 @@ export function withPlatformNavigation() { if (!(locationInstance instanceof ɵNavigationAdapterForLocation)) { const locationConstructorName = (locationInstance as any).constructor.name; let message = - `'withPlatformNavigation' provides a 'Location' implementation that ensures navigation APIs are consistently used.` + + `'withExperimentalPlatformNavigation' provides a 'Location' implementation that ensures navigation APIs are consistently used.` + ` An instance of ${locationConstructorName} was found instead.`; if (locationConstructorName === 'SpyLocation') { - message += ` One of 'RouterTestingModule' or 'provideLocationMocks' was likely used. 'withPlatformNavigation' does not work with these because they override the Location implementation.`; + message += ` One of 'RouterTestingModule' or 'provideLocationMocks' was likely used. 'withExperimentalPlatformNavigation' does not work with these because they override the Location implementation.`; } throw new Error(message); } @@ -285,7 +313,7 @@ export function withPlatformNavigation() { {provide: Location, useClass: ɵNavigationAdapterForLocation}, devModeLocationCheck, ]; - return routerFeature(RouterFeatureKind.InMemoryScrollingFeature, providers); + return routerFeature(RouterFeatureKind.ExperimentalPlatformNavigationFeature, providers); } export function getBootstrapListener() { @@ -910,7 +938,8 @@ export type RouterFeatures = | ComponentInputBindingFeature | ViewTransitionsFeature | ExperimentalAutoCleanupInjectorsFeature - | RouterHashLocationFeature; + | RouterHashLocationFeature + | ExperimentalPlatformNavigationFeature; /** * The list of features as an enum to uniquely type each feature. @@ -927,4 +956,5 @@ export const enum RouterFeatureKind { ComponentInputBindingFeature, ViewTransitionsFeature, ExperimentalAutoCleanupInjectorsFeature, + ExperimentalPlatformNavigationFeature, } diff --git a/packages/router/test/computed_state_restoration.spec.ts b/packages/router/test/computed_state_restoration.spec.ts index 6c8e975d6b7..a30f19c4b4a 100644 --- a/packages/router/test/computed_state_restoration.spec.ts +++ b/packages/router/test/computed_state_restoration.spec.ts @@ -13,7 +13,7 @@ import {expect} from '@angular/private/testing/matchers'; import {Router, RouterModule, RouterOutlet, UrlTree, withRouterConfig} from '../index'; import {EMPTY, of} from 'rxjs'; -import {provideRouter, withPlatformNavigation} from '../src/provide_router'; +import {provideRouter, withExperimentalPlatformNavigation} from '../src/provide_router'; import {isUrlTree} from '../src/url_tree'; import {timeout, useAutoTick} from './helpers'; import {afterNextNavigation} from '../src/utils/navigations'; @@ -127,7 +127,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { resolveNavigationPromiseOnError: true, }), browserAPI === 'navigation' - ? withPlatformNavigation() + ? withExperimentalPlatformNavigation() : (makeEnvironmentProviders([]) as any), ), ], diff --git a/packages/router/test/integration/integration.spec.ts b/packages/router/test/integration/integration.spec.ts index 54a11c34edf..fe4ab08246b 100644 --- a/packages/router/test/integration/integration.spec.ts +++ b/packages/router/test/integration/integration.spec.ts @@ -35,7 +35,7 @@ import { RoutesRecognized, } from '../../index'; -import {provideRouter, withPlatformNavigation} from '../../src/provide_router'; +import {provideRouter, withExperimentalPlatformNavigation} from '../../src/provide_router'; import { BlankCmp, CollectParamsCmp, @@ -87,7 +87,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { provideRouter( [{path: 'simple', component: SimpleCmp}], browserAPI === 'navigation' - ? withPlatformNavigation() + ? withExperimentalPlatformNavigation() : (makeEnvironmentProviders([]) as any), ), ], diff --git a/packages/router/test/with_platform_navigation.spec.ts b/packages/router/test/with_platform_navigation.spec.ts index 6ac226f6040..bda2980d8b3 100644 --- a/packages/router/test/with_platform_navigation.spec.ts +++ b/packages/router/test/with_platform_navigation.spec.ts @@ -8,7 +8,7 @@ import {TestBed} from '@angular/core/testing'; import {provideRouter, Router} from '../src'; -import {withPlatformNavigation, withRouterConfig} from '../src/provide_router'; +import {withExperimentalPlatformNavigation, withRouterConfig} from '../src/provide_router'; import {withBody} from '@angular/private/testing'; import { PlatformLocation, @@ -27,7 +27,9 @@ import {timeout, useAutoTick} from './helpers'; describe('withPlatformNavigation feature', () => { beforeEach(() => { - TestBed.configureTestingModule({providers: [provideRouter([], withPlatformNavigation())]}); + TestBed.configureTestingModule({ + providers: [provideRouter([], withExperimentalPlatformNavigation())], + }); }); it('provides FakeNavigation by default', () => { @@ -170,7 +172,7 @@ describe('withPlatformNavigation feature', () => { providers: [ provideRouter( [{path: '**', children: []}], - withPlatformNavigation(), + withExperimentalPlatformNavigation(), withRouterConfig({urlUpdateStrategy: 'eager'}), ), ], @@ -203,7 +205,7 @@ describe('withPlatformNavigation feature', () => { describe('configuration error', () => { it('throws an error mentioning SpyLocation and the location mocks', () => { TestBed.configureTestingModule({ - providers: [provideRouter([], withPlatformNavigation()), provideLocationMocks()], + providers: [provideRouter([], withExperimentalPlatformNavigation()), provideLocationMocks()], }); expect(() => TestBed.inject(Location)).toThrowError(/SpyLocation.*provideLocationMocks/); }); @@ -215,7 +217,7 @@ if (typeof window !== 'undefined' && 'navigation' in window) { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - provideRouter([{path: '**', children: []}], withPlatformNavigation()), + provideRouter([{path: '**', children: []}], withExperimentalPlatformNavigation()), {provide: PlatformLocation, useClass: BrowserPlatformLocation}, {provide: PlatformNavigation, useFactory: () => navigation}, ],