Skip to content

Commit 97cac1c

Browse files
SkyZeroZxamishne
authored andcommitted
fix(common): prevent focus from scrollToAnchor
Focus the target element using `focus({preventScroll: true})` after scrolling, so the browser doesn’t adjust the scroll position when applying focus. Fixes #65938
1 parent f5b139f commit 97cac1c

2 files changed

Lines changed: 30 additions & 4 deletions

File tree

packages/common/src/viewport_scroller.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export abstract class ViewportScroller {
5555
/**
5656
* Scrolls to an anchor element.
5757
* @param anchor The ID of the anchor element.
58+
* @param options Scroll options
5859
*/
5960
abstract scrollToAnchor(anchor: string, options?: ScrollOptions): void;
6061

@@ -125,11 +126,12 @@ export class BrowserViewportScroller implements ViewportScroller {
125126
this.scrollToElement(elSelected, options);
126127
// After scrolling to the element, the spec dictates that we follow the focus steps for the
127128
// target. Rather than following the robust steps, simply attempt focus.
128-
//
129+
// Use `preventScroll: true` to avoid extra scroll that breaks smooth scrolling.
129130
// @see https://html.spec.whatwg.org/#get-the-focusable-area
130131
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus
131132
// @see https://html.spec.whatwg.org/#focusable-area
132-
elSelected.focus();
133+
// @see https://www.yanandcoffee.com/2020/05/08/accessible-smooth-scrolling-and-focus-management-solutions/
134+
elSelected.focus({preventScroll: true});
133135
}
134136
}
135137

@@ -235,7 +237,7 @@ export class NullViewportScroller implements ViewportScroller {
235237
/**
236238
* Empty implementation
237239
*/
238-
scrollToAnchor(anchor: string): void {}
240+
scrollToAnchor(anchor: string, options?: ScrollOptions): void {}
239241

240242
/**
241243
* Empty implementation

packages/common/test/viewport_scroller_spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
10-
import {isNode} from '@angular/private/testing';
10+
import {isNode, waitFor} from '@angular/private/testing';
1111

1212
describe('BrowserViewportScroller', () => {
1313
describe('setHistoryScrollRestoration', () => {
@@ -112,6 +112,30 @@ describe('BrowserViewportScroller', () => {
112112
cleanup();
113113
});
114114

115+
it('should honor the scroll offset when smooth scrolling', async () => {
116+
// Ensure the scroll behavior is smooth for this test, as the bug only occurred with smooth scrolling.
117+
document.documentElement.style.scrollBehavior = 'smooth';
118+
119+
const {anchorNode, cleanup} = createTallElement();
120+
anchorNode.id = anchor;
121+
122+
// Padding ensures the page is tall enough that the offset-adjusted target
123+
// is reachable and not clamped to the maximum scroll position.
124+
document.body.style.paddingBottom = '5000px';
125+
// Header offset
126+
scroller.setOffset([0, 80]);
127+
128+
scroller.scrollToAnchor(anchor);
129+
130+
await waitFor(() => throwUnless(anchorNode.getBoundingClientRect().top).toBe(80), {
131+
timeout: 1_000,
132+
});
133+
134+
document.documentElement.style.scrollBehavior = '';
135+
document.body.style.paddingBottom = '';
136+
cleanup();
137+
});
138+
115139
function createTallElement() {
116140
const tallItem = document.createElement('div');
117141
tallItem.style.height = '3000px';

0 commit comments

Comments
 (0)