Skip to content

Commit 1f055b9

Browse files
atscottAndrewKushnir
authored andcommitted
fix(router): Ensure anchor scrolling happens on ignored same URL navigations (#48025)
The Router scroller only listens for NavigationEnd events. However, the default behavior of the Router is to ignore navigations to the same URL. This breaks the anchor scrolling when clicking on an anchor whose fragment is already in the URL. fixes #29099 BREAKING CHANGE: The `Scroll` event's `routerEvent` property may also be a `NavigationSkipped` event. Previously, it was only a `NavigationEnd` event. PR Close #48025
1 parent 48aa96e commit 1f055b9

File tree

4 files changed

+21
-12
lines changed

4 files changed

+21
-12
lines changed

goldens/public-api/router/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -950,15 +950,15 @@ export type RunGuardsAndResolvers = 'pathParamsChange' | 'pathParamsOrQueryParam
950950
// @public
951951
export class Scroll {
952952
constructor(
953-
routerEvent: NavigationEnd,
953+
routerEvent: NavigationEnd | NavigationSkipped,
954954
position: [number, number] | null,
955955
anchor: string | null);
956956
// (undocumented)
957957
readonly anchor: string | null;
958958
// (undocumented)
959959
readonly position: [number, number] | null;
960960
// (undocumented)
961-
readonly routerEvent: NavigationEnd;
961+
readonly routerEvent: NavigationEnd | NavigationSkipped;
962962
// (undocumented)
963963
toString(): string;
964964
// (undocumented)

packages/router/src/events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ export class Scroll {
582582

583583
constructor(
584584
/** @docsNotRequired */
585-
readonly routerEvent: NavigationEnd,
585+
readonly routerEvent: NavigationEnd|NavigationSkipped,
586586

587587
/** @docsNotRequired */
588588
readonly position: [number, number]|null,

packages/router/src/router_scroller.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import {ViewportScroller} from '@angular/common';
1010
import {Injectable, InjectionToken, NgZone, OnDestroy} from '@angular/core';
1111
import {Unsubscribable} from 'rxjs';
1212

13-
import {NavigationEnd, NavigationStart, Scroll} from './events';
13+
import {NavigationEnd, NavigationSkipped, NavigationSkippedCode, NavigationStart, Scroll} from './events';
1414
import {NavigationTransitions} from './navigation_transition';
15-
import {Router} from './router';
1615
import {UrlSerializer} from './url_tree';
1716

1817
export const ROUTER_SCROLLER = new InjectionToken<RouterScroller>('');
@@ -61,6 +60,12 @@ export class RouterScroller implements OnDestroy {
6160
} else if (e instanceof NavigationEnd) {
6261
this.lastId = e.id;
6362
this.scheduleScrollEvent(e, this.urlSerializer.parse(e.urlAfterRedirects).fragment);
63+
} else if (
64+
e instanceof NavigationSkipped &&
65+
e.code === NavigationSkippedCode.IgnoredSameUrlNavigation) {
66+
this.lastSource = undefined;
67+
this.restoredId = 0;
68+
this.scheduleScrollEvent(e, this.urlSerializer.parse(e.url).fragment);
6469
}
6570
});
6671
}
@@ -86,7 +91,8 @@ export class RouterScroller implements OnDestroy {
8691
});
8792
}
8893

89-
private scheduleScrollEvent(routerEvent: NavigationEnd, anchor: string|null): void {
94+
private scheduleScrollEvent(routerEvent: NavigationEnd|NavigationSkipped, anchor: string|null):
95+
void {
9096
this.zone.runOutsideAngular(() => {
9197
// The scroll event needs to be delayed until after change detection. Otherwise, we may
9298
// attempt to restore the scroll position before the router outlet has fully rendered the

packages/router/test/bootstrap.spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ describe('bootstrap', () => {
455455
scrollPositionRestoration: 'enabled',
456456
anchorScrolling: 'enabled',
457457
scrollOffset: [0, 100],
458-
onSameUrlNavigation: 'reload'
458+
onSameUrlNavigation: 'ignore',
459459
})
460460
],
461461
declarations: [TallComponent, RootCmp],
@@ -494,13 +494,16 @@ describe('bootstrap', () => {
494494

495495
await router.navigateByUrl('/aa#marker2');
496496
await resolveAfter(100);
497-
expect(window.pageYOffset).toBeGreaterThanOrEqual(5900);
498-
expect(window.pageYOffset).toBeLessThan(6000); // offset
497+
expect(window.scrollY).toBeGreaterThanOrEqual(5900);
498+
expect(window.scrollY).toBeLessThan(6000); // offset
499499

500-
await router.navigateByUrl('/aa#marker3');
500+
// Scroll somewhere else, then navigate to the hash again. Even though the same url navigation
501+
// is ignored by the Router, we should still scroll.
502+
window.scrollTo(0, 3000);
503+
await router.navigateByUrl('/aa#marker2');
501504
await resolveAfter(100);
502-
expect(window.pageYOffset).toBeGreaterThanOrEqual(8900);
503-
expect(window.pageYOffset).toBeLessThan(9000);
505+
expect(window.scrollY).toBeGreaterThanOrEqual(5900);
506+
expect(window.scrollY).toBeLessThan(6000); // offset
504507
});
505508

506509
it('should cleanup "popstate" and "hashchange" listeners', async () => {

0 commit comments

Comments
 (0)