diff --git a/packages/router/src/router_scroller.ts b/packages/router/src/router_scroller.ts index 03b87b00f62..5e2f5d01cee 100644 --- a/packages/router/src/router_scroller.ts +++ b/packages/router/src/router_scroller.ts @@ -7,7 +7,14 @@ */ import {ViewportScroller} from '@angular/common'; -import {Injectable, InjectionToken, NgZone, OnDestroy} from '@angular/core'; +import { + EnvironmentInjector, + inject, + Injectable, + InjectionToken, + NgZone, + OnDestroy, +} from '@angular/core'; import {Unsubscribable} from 'rxjs'; import { @@ -105,21 +112,30 @@ export class RouterScroller implements OnDestroy { routerEvent: NavigationEnd | NavigationSkipped, anchor: string | null, ): void { - this.zone.runOutsideAngular(() => { + this.zone.runOutsideAngular(async () => { // The scroll event needs to be delayed until after change detection. Otherwise, we may // attempt to restore the scroll position before the router outlet has fully rendered the // component by executing its update block of the template function. - setTimeout(() => { - this.zone.run(() => { - this.transitions.events.next( - new Scroll( - routerEvent, - this.lastSource === 'popstate' ? this.store[this.restoredId] : null, - anchor, - ), - ); - }); - }, 0); + // + // #57109 (we need to wait at least a macrotask before scrolling. AfterNextRender resolves in microtask event loop with Zones) + // We could consider _also_ waiting for a render promise though one should have already happened or been scheduled by this point + // and should definitely happen before rAF/setTimeout. + // #53985 (cannot rely solely on setTimeout because a frame may paint before the timeout) + await new Promise((resolve) => { + setTimeout(resolve); + if (typeof requestAnimationFrame !== 'undefined') { + requestAnimationFrame(resolve); + } + }); + this.zone.run(() => { + this.transitions.events.next( + new Scroll( + routerEvent, + this.lastSource === 'popstate' ? this.store[this.restoredId] : null, + anchor, + ), + ); + }); }); }