From bf8cef9dd64a949373ad1e758ab05689dc280d6b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 10 Apr 2025 09:43:33 +0200 Subject: [PATCH] fix(material/bottom-sheet): page jumping if backdrop-filter is applied The bottom sheet has an animation where it starts off-screen and animates in. At the same time it moves focus into itself. It seems like under certain conditions (e.g. having `backdrop-filter` on the `body`) this causes the entire page the jump in a jarring way due to the focus being moved. These changes resolve the issue by telling the browser not to scroll the page when moving focus. Fixes #30774. --- goldens/cdk/dialog/index.api.md | 4 ++-- goldens/material/bottom-sheet/index.api.md | 2 ++ src/cdk/dialog/dialog-container.ts | 18 +++++++++--------- .../bottom-sheet/bottom-sheet-container.ts | 9 +++++++++ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/goldens/cdk/dialog/index.api.md b/goldens/cdk/dialog/index.api.md index cbcca84d68f1..215130c04d30 100644 --- a/goldens/cdk/dialog/index.api.md +++ b/goldens/cdk/dialog/index.api.md @@ -58,7 +58,7 @@ export class CdkDialogContainer extends B // (undocumented) protected _document: Document; // (undocumented) - protected _elementRef: ElementRef; + protected _elementRef: ElementRef; // (undocumented) protected _focusTrapFactory: FocusTrapFactory; // (undocumented) @@ -69,7 +69,7 @@ export class CdkDialogContainer extends B _recaptureFocus(): void; // (undocumented) _removeAriaLabelledBy(id: string): void; - protected _trapFocus(): void; + protected _trapFocus(options?: FocusOptions): void; // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-dialog-container", never, {}, {}, never, never, true, never>; // (undocumented) diff --git a/goldens/material/bottom-sheet/index.api.md b/goldens/material/bottom-sheet/index.api.md index fd8cdf9eb1d9..a5fdf244c147 100644 --- a/goldens/material/bottom-sheet/index.api.md +++ b/goldens/material/bottom-sheet/index.api.md @@ -88,6 +88,8 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes // (undocumented) ngOnDestroy(): void; // (undocumented) + protected _trapFocus(): void; + // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; diff --git a/src/cdk/dialog/dialog-container.ts b/src/cdk/dialog/dialog-container.ts index 38836bdda0fd..520d68d82fec 100644 --- a/src/cdk/dialog/dialog-container.ts +++ b/src/cdk/dialog/dialog-container.ts @@ -73,7 +73,7 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { - protected _elementRef = inject(ElementRef); + protected _elementRef = inject>(ElementRef); protected _focusTrapFactory = inject(FocusTrapFactory); readonly _config: C; private _interactivityChecker = inject(InteractivityChecker); @@ -254,7 +254,7 @@ export class CdkDialogContainer * Moves the focus inside the focus trap. When autoFocus is not set to 'dialog', if focus * cannot be moved then focus will go to the dialog container. */ - protected _trapFocus() { + protected _trapFocus(options?: FocusOptions) { if (this._isDestroyed) { return; } @@ -274,23 +274,23 @@ export class CdkDialogContainer // if the focus isn't inside the dialog already, because it's possible that the consumer // turned off `autoFocus` in order to move focus themselves. if (!this._containsFocus()) { - element.focus(); + element.focus(options); } break; case true: case 'first-tabbable': - const focusedSuccessfully = this._focusTrap?.focusInitialElement(); + const focusedSuccessfully = this._focusTrap?.focusInitialElement(options); // If we weren't able to find a focusable element in the dialog, then focus the dialog // container instead. if (!focusedSuccessfully) { - this._focusDialogContainer(); + this._focusDialogContainer(options); } break; case 'first-heading': - this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]'); + this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]', options); break; default: - this._focusByCssSelector(this._config.autoFocus!); + this._focusByCssSelector(this._config.autoFocus!, options); break; } }, @@ -345,10 +345,10 @@ export class CdkDialogContainer } /** Focuses the dialog container. */ - private _focusDialogContainer() { + private _focusDialogContainer(options?: FocusOptions) { // Note that there is no focus method when rendering on the server. if (this._elementRef.nativeElement.focus) { - this._elementRef.nativeElement.focus(); + this._elementRef.nativeElement.focus(options); } } diff --git a/src/material/bottom-sheet/bottom-sheet-container.ts b/src/material/bottom-sheet/bottom-sheet-container.ts index 7c3cdb0c014c..66dddd36b02c 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.ts +++ b/src/material/bottom-sheet/bottom-sheet-container.ts @@ -132,6 +132,15 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes }); } + protected override _trapFocus(): void { + // The bottom sheet starts off-screen and animates in, and at the same time we trap focus + // within it. With some styles this appears to cause the page to jump around. See: + // https://github.com/angular/components/issues/30774. Preventing the browser from + // scrolling resolves the issue and isn't really necessary since the bottom sheet + // normally isn't scrollable. + super._trapFocus({preventScroll: true}); + } + protected _handleAnimationEvent(isStart: boolean, animationName: string) { const isEnter = animationName === ENTER_ANIMATION; const isExit = animationName === EXIT_ANIMATION;