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

(cherry picked from commit 97cac1cf4d)
This commit is contained in:
SkyZeroZx 2026-04-21 13:17:02 -05:00 committed by Alon Mishne
parent d07f502946
commit 10ad3c0692
2 changed files with 30 additions and 4 deletions

View file

@ -55,6 +55,7 @@ export abstract class ViewportScroller {
/**
* Scrolls to an anchor element.
* @param anchor The ID of the anchor element.
* @param options Scroll options
*/
abstract scrollToAnchor(anchor: string, options?: ScrollOptions): void;
@ -125,11 +126,12 @@ export class BrowserViewportScroller implements ViewportScroller {
this.scrollToElement(elSelected, options);
// After scrolling to the element, the spec dictates that we follow the focus steps for the
// target. Rather than following the robust steps, simply attempt focus.
//
// Use `preventScroll: true` to avoid extra scroll that breaks smooth scrolling.
// @see https://html.spec.whatwg.org/#get-the-focusable-area
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus
// @see https://html.spec.whatwg.org/#focusable-area
elSelected.focus();
// @see https://www.yanandcoffee.com/2020/05/08/accessible-smooth-scrolling-and-focus-management-solutions/
elSelected.focus({preventScroll: true});
}
}
@ -235,7 +237,7 @@ export class NullViewportScroller implements ViewportScroller {
/**
* Empty implementation
*/
scrollToAnchor(anchor: string): void {}
scrollToAnchor(anchor: string, options?: ScrollOptions): void {}
/**
* Empty implementation

View file

@ -7,7 +7,7 @@
*/
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
import {isNode} from '@angular/private/testing';
import {isNode, waitFor} from '@angular/private/testing';
describe('BrowserViewportScroller', () => {
describe('setHistoryScrollRestoration', () => {
@ -112,6 +112,30 @@ describe('BrowserViewportScroller', () => {
cleanup();
});
it('should honor the scroll offset when smooth scrolling', async () => {
// Ensure the scroll behavior is smooth for this test, as the bug only occurred with smooth scrolling.
document.documentElement.style.scrollBehavior = 'smooth';
const {anchorNode, cleanup} = createTallElement();
anchorNode.id = anchor;
// Padding ensures the page is tall enough that the offset-adjusted target
// is reachable and not clamped to the maximum scroll position.
document.body.style.paddingBottom = '5000px';
// Header offset
scroller.setOffset([0, 80]);
scroller.scrollToAnchor(anchor);
await waitFor(() => throwUnless(anchorNode.getBoundingClientRect().top).toBe(80), {
timeout: 1_000,
});
document.documentElement.style.scrollBehavior = '';
document.body.style.paddingBottom = '';
cleanup();
});
function createTallElement() {
const tallItem = document.createElement('div');
tallItem.style.height = '3000px';