From 22596a7b17b873a8d298fe6b99b72a202362b627 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 31 Oct 2025 09:49:11 +0100 Subject: [PATCH 1/4] fix(cdk/overlay): simplify public API of overlay directive Currently the `CdkConnectedOverlay` directive has 23 inputs which can be quite verbose. These changes add a shorthand through the `cdkConnectedOverlay` attribute for the cases where these values aren't expected to change. --- goldens/cdk/overlay/index.api.md | 56 ++++++++++++++++- src/cdk/overlay/overlay-directives.ts | 86 ++++++++++++++++++++++----- src/cdk/overlay/public-api.ts | 6 +- 3 files changed, 129 insertions(+), 19 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index 1b556164f5ed..f5f9638470ba 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -44,12 +44,12 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { attachOverlay(): void; backdropClass: string | string[]; readonly backdropClick: EventEmitter; + set _config(value: string | CdkConnectedOverlayConfig); readonly detach: EventEmitter; detachOverlay(): void; get dir(): Direction; disableClose: boolean; - get disposeOnNavigation(): boolean; - set disposeOnNavigation(value: boolean); + disposeOnNavigation: boolean; flexibleDimensions: boolean; growAfterOpen: boolean; hasBackdrop: boolean; @@ -95,11 +95,61 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { viewportMargin: ViewportMargin; width: number | string; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export interface CdkConnectedOverlayConfig { + // (undocumented) + backdropClass?: string | string[]; + // (undocumented) + disableClose?: boolean; + // (undocumented) + disposeOnNavigation?: boolean; + // (undocumented) + flexibleDimensions?: boolean; + // (undocumented) + growAfterOpen?: boolean; + // (undocumented) + hasBackdrop?: boolean; + // (undocumented) + height?: number | string; + // (undocumented) + lockPosition?: boolean; + // (undocumented) + matchWidth?: boolean; + // (undocumented) + minHeight?: number | string; + // (undocumented) + minWidth?: number | string; + // (undocumented) + offsetX?: number; + // (undocumented) + offsetY?: number; + // (undocumented) + origin?: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin; + // (undocumented) + panelClass?: string | string[]; + // (undocumented) + positions?: ConnectedPosition[]; + // (undocumented) + positionStrategy?: FlexibleConnectedPositionStrategy; + // (undocumented) + push?: boolean; + // (undocumented) + scrollStrategy?: ScrollStrategy; + // (undocumented) + transformOriginSelector?: string; + // (undocumented) + usePopover?: boolean; + // (undocumented) + viewportMargin?: ViewportMargin; + // (undocumented) + width?: number | string; +} + // @public export class CdkOverlayOrigin { constructor(...args: unknown[]); diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 62ed6d4ab868..71f645d82f5f 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -96,6 +96,33 @@ export class CdkOverlayOrigin { constructor() {} } +/** Object used to configure the `CdkConnectedOverlay` directive. */ +export interface CdkConnectedOverlayConfig { + origin?: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin; + positions?: ConnectedPosition[]; + positionStrategy?: FlexibleConnectedPositionStrategy; + offsetX?: number; + offsetY?: number; + width?: number | string; + height?: number | string; + minWidth?: number | string; + minHeight?: number | string; + backdropClass?: string | string[]; + panelClass?: string | string[]; + viewportMargin?: ViewportMargin; + scrollStrategy?: ScrollStrategy; + disableClose?: boolean; + transformOriginSelector?: string; + hasBackdrop?: boolean; + lockPosition?: boolean; + flexibleDimensions?: boolean; + growAfterOpen?: boolean; + push?: boolean; + disposeOnNavigation?: boolean; + usePopover?: boolean; + matchWidth?: boolean; +} + /** * Directive to facilitate declarative creation of an * Overlay using a FlexibleConnectedPositionStrategy. @@ -118,7 +145,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { private _offsetY: number; private _position: FlexibleConnectedPositionStrategy; private _scrollStrategyFactory = inject(CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY); - private _disposeOnNavigation = false; private _ngZone = inject(NgZone); /** Origin for the connected overlay. */ @@ -214,17 +240,20 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { /** Whether the overlay should be disposed of when the user goes backwards/forwards in history. */ @Input({alias: 'cdkConnectedOverlayDisposeOnNavigation', transform: booleanAttribute}) - get disposeOnNavigation(): boolean { - return this._disposeOnNavigation; - } - set disposeOnNavigation(value: boolean) { - this._disposeOnNavigation = value; - } + disposeOnNavigation: boolean = false; /** Whether the connected overlay should be rendered inside a popover element or the overlay container. */ @Input({alias: 'cdkConnectedOverlayUsePopover', transform: booleanAttribute}) usePopover: boolean = false; + /** Shorthand for setting multiple overlay options at once. */ + @Input('cdkConnectedOverlay') + set _config(value: string | CdkConnectedOverlayConfig) { + if (typeof value !== 'string') { + this._assignConfig(value); + } + } + /** Event emitted when the backdrop is clicked. */ @Output() readonly backdropClick = new EventEmitter(); @@ -419,19 +448,21 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { attachOverlay() { if (!this._overlayRef) { this._createOverlay(); - } else { - // Update the overlay size, in case the directive's inputs have changed - this._overlayRef.getConfig().hasBackdrop = this.hasBackdrop; } - if (!this._overlayRef!.hasAttached()) { - this._overlayRef!.attach(this._templatePortal); + const ref = this._overlayRef!; + + // Update the overlay size, in case the directive's inputs have changed + ref.getConfig().hasBackdrop = this.hasBackdrop; + + if (!ref.hasAttached()) { + ref.attach(this._templatePortal); } if (this.hasBackdrop) { - this._backdropSubscription = this._overlayRef!.backdropClick().subscribe(event => { - this.backdropClick.emit(event); - }); + this._backdropSubscription = ref + .backdropClick() + .subscribe(event => this.backdropClick.emit(event)); } else { this._backdropSubscription.unsubscribe(); } @@ -462,4 +493,29 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { this._positionSubscription.unsubscribe(); this.open = false; } + + private _assignConfig(config: CdkConnectedOverlayConfig) { + this.origin = config.origin ?? this.origin; + this.positions = config.positions ?? this.positions; + this.positionStrategy = config.positionStrategy ?? this.positionStrategy; + this.offsetX = config.offsetX ?? this.offsetX; + this.offsetY = config.offsetY ?? this.offsetY; + this.width = config.width ?? this.width; + this.height = config.height ?? this.height; + this.minWidth = config.minWidth ?? this.minWidth; + this.minHeight = config.minHeight ?? this.minHeight; + this.backdropClass = config.backdropClass ?? this.backdropClass; + this.panelClass = config.panelClass ?? this.panelClass; + this.viewportMargin = config.viewportMargin ?? this.viewportMargin; + this.scrollStrategy = config.scrollStrategy ?? this.scrollStrategy; + this.disableClose = config.disableClose ?? this.disableClose; + this.transformOriginSelector = config.transformOriginSelector ?? this.transformOriginSelector; + this.hasBackdrop = config.hasBackdrop ?? this.hasBackdrop; + this.lockPosition = config.lockPosition ?? this.lockPosition; + this.flexibleDimensions = config.flexibleDimensions ?? this.flexibleDimensions; + this.growAfterOpen = config.growAfterOpen ?? this.growAfterOpen; + this.push = config.push ?? this.push; + this.disposeOnNavigation = config.disposeOnNavigation ?? this.disposeOnNavigation; + this.usePopover = config.usePopover ?? this.usePopover; + } } diff --git a/src/cdk/overlay/public-api.ts b/src/cdk/overlay/public-api.ts index 43a89dfdffea..ee673e9facda 100644 --- a/src/cdk/overlay/public-api.ts +++ b/src/cdk/overlay/public-api.ts @@ -13,7 +13,11 @@ export * from './overlay-module'; export * from './dispatchers/index'; export {Overlay, createOverlayRef} from './overlay'; export {OverlayContainer} from './overlay-container'; -export {CdkOverlayOrigin, CdkConnectedOverlay} from './overlay-directives'; +export { + CdkOverlayOrigin, + CdkConnectedOverlay, + CdkConnectedOverlayConfig, +} from './overlay-directives'; export {FullscreenOverlayContainer} from './fullscreen-overlay-container'; export {OverlayRef, OverlaySizeConfig} from './overlay-ref'; export {ViewportRuler} from '../scrolling'; From d28caf74a11a7d4165c5879a8d9b8ee30007b464 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 31 Oct 2025 10:00:05 +0100 Subject: [PATCH 2/4] fix(cdk/overlay): simplify matching the overlay to the trigger width The connected overlay directive allows users to control its width using the `cdkConnectedOverlayMatchWidth` input. This can be tricky when it needs to match another element like the trigger. Since this is a common for cases like dropdowns, these changes add the `cdkConnectedOverlayMatchWidth` input to simplify it. --- goldens/cdk/overlay/index.api.md | 5 +++- src/cdk/overlay/overlay-directives.spec.ts | 28 +++++++++++++++++----- src/cdk/overlay/overlay-directives.ts | 21 ++++++++++++---- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index f5f9638470ba..a4d2a4dc7861 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -55,6 +55,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { hasBackdrop: boolean; height: number | string; lockPosition: boolean; + matchWidth: boolean; minHeight: number | string; minWidth: number | string; // (undocumented) @@ -68,6 +69,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { // (undocumented) static ngAcceptInputType_lockPosition: unknown; // (undocumented) + static ngAcceptInputType_matchWidth: unknown; + // (undocumented) static ngAcceptInputType_push: unknown; // (undocumented) static ngAcceptInputType_usePopover: unknown; @@ -95,7 +98,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { viewportMargin: ViewportMargin; width: number | string; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } diff --git a/src/cdk/overlay/overlay-directives.spec.ts b/src/cdk/overlay/overlay-directives.spec.ts index 22a161ba1889..ef11a388e13a 100644 --- a/src/cdk/overlay/overlay-directives.spec.ts +++ b/src/cdk/overlay/overlay-directives.spec.ts @@ -626,6 +626,19 @@ describe('Overlay directives', () => { expect(target.style.transformOrigin).toContain('left bottom'); }); + + it('should match the trigger width', () => { + const trigger = fixture.nativeElement.querySelector('#trigger') as HTMLElement; + trigger.style.width = '128px'; + + fixture.componentInstance.matchWidth = true; + fixture.componentInstance.isOpen = true; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; + expect(pane.style.width).toBe('128px'); + }); }); describe('outputs', () => { @@ -742,11 +755,11 @@ describe('Overlay directives', () => { @Component({ template: ` - - + + - { [cdkConnectedOverlayMinWidth]="minWidth" [cdkConnectedOverlayMinHeight]="minHeight" [cdkConnectedOverlayPositions]="positionOverrides" - [cdkConnectedOverlayTransformOriginOn]="transformOriginSelector"> + [cdkConnectedOverlayTransformOriginOn]="transformOriginSelector" + [cdkConnectedOverlayMatchWidth]="matchWidth">

Menu content

`, imports: [OverlayModule], @@ -809,12 +823,14 @@ class ConnectedOverlayDirectiveTest { detachHandler = jasmine.createSpy('detachHandler'); attachResult: HTMLElement; transformOriginSelector: string; + matchWidth = false; } @Component({ template: ` - - Menu content`, + + Menu content + `, imports: [OverlayModule], }) class ConnectedOverlayPropertyInitOrder { diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 71f645d82f5f..165d47936158 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -246,6 +246,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { @Input({alias: 'cdkConnectedOverlayUsePopover', transform: booleanAttribute}) usePopover: boolean = false; + /** Whether the overlay should match the trigger's width. */ + @Input({alias: 'cdkConnectedOverlayMatchWidth', transform: booleanAttribute}) + matchWidth: boolean = false; + /** Shorthand for setting multiple overlay options at once. */ @Input('cdkConnectedOverlay') set _config(value: string | CdkConnectedOverlayConfig) { @@ -306,7 +310,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { if (this._position) { this._updatePositionStrategy(this._position); this._overlayRef?.updateSize({ - width: this.width, + width: this._getWidth(), minWidth: this.minWidth, height: this.height, minHeight: this.minHeight, @@ -363,10 +367,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { usePopover: this.usePopover, }); - if (this.width || this.width === 0) { - overlayConfig.width = this.width; - } - if (this.height || this.height === 0) { overlayConfig.height = this.height; } @@ -444,6 +444,15 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { return null; } + private _getWidth() { + if (this.width) { + return this.width; + } + + // Null check `getBoundingClientRect` in case this is called during SSR. + return this.matchWidth ? this._getOriginElement()?.getBoundingClientRect?.().width : undefined; + } + /** Attaches the overlay. */ attachOverlay() { if (!this._overlayRef) { @@ -454,6 +463,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { // Update the overlay size, in case the directive's inputs have changed ref.getConfig().hasBackdrop = this.hasBackdrop; + ref.updateSize({width: this._getWidth()}); if (!ref.hasAttached()) { ref.attach(this._templatePortal); @@ -517,5 +527,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { this.push = config.push ?? this.push; this.disposeOnNavigation = config.disposeOnNavigation ?? this.disposeOnNavigation; this.usePopover = config.usePopover ?? this.usePopover; + this.matchWidth = config.matchWidth ?? this.matchWidth; } } From e8e38f55cfd6703f9095e72ddb075a7a0eb0f640 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 31 Oct 2025 10:07:01 +0100 Subject: [PATCH 3/4] fix(cdk/overlay): hide native backdrop Explicitly hides the native backdrop from the CDK popover so it doesn't get rendered out. --- src/cdk/overlay/_index.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cdk/overlay/_index.scss b/src/cdk/overlay/_index.scss index 753cd40a0464..50c929a0665d 100644 --- a/src/cdk/overlay/_index.scss +++ b/src/cdk/overlay/_index.scss @@ -204,6 +204,11 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default; // with `align-self` can break the positioning (see #29809). inset: auto; + // For the time being we're using our `.cdk-overlay-backdrop` element instead of the native one. + &::backdrop { + display: none; + } + .cdk-overlay-backdrop { position: fixed; } From 0811d2c25ea3264d822170a19c7eaad3b279f0b1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 31 Oct 2025 16:19:32 +0100 Subject: [PATCH 4/4] fix(cdk/overlay): make it easier to set default for overlay directive Adds the `CDK_CONNECTED_OVERLAY_DEFAULT_CONFIG` injection token to make it easier to configure the `CdkConnectedOverlay` directive. --- goldens/cdk/overlay/index.api.md | 4 ++++ src/cdk/overlay/overlay-directives.ts | 13 +++++++++++++ src/cdk/overlay/public-api.ts | 1 + 3 files changed, 18 insertions(+) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index a4d2a4dc7861..f3d7a5f9ff45 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -12,6 +12,7 @@ import { EmbeddedViewRef } from '@angular/core'; import { EnvironmentInjector } from '@angular/core'; import { EventEmitter } from '@angular/core'; import * as i0 from '@angular/core'; +import { InjectionToken } from '@angular/core'; import { Injector } from '@angular/core'; import { Location as Location_2 } from '@angular/common'; import { NgIterable } from '@angular/core'; @@ -37,6 +38,9 @@ export class BlockScrollStrategy implements ScrollStrategy { enable(): void; } +// @public +export const CDK_CONNECTED_OVERLAY_DEFAULT_CONFIG: InjectionToken; + // @public export class CdkConnectedOverlay implements OnDestroy, OnChanges { constructor(...args: unknown[]); diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 165d47936158..83ccf2a008c9 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -96,6 +96,14 @@ export class CdkOverlayOrigin { constructor() {} } +/** + * Injection token that can be used to configure the + * default options for the `CdkConnectedOverlay` directive. + */ +export const CDK_CONNECTED_OVERLAY_DEFAULT_CONFIG = new InjectionToken( + 'cdk-connected-overlay-default-config', +); + /** Object used to configure the `CdkConnectedOverlay` directive. */ export interface CdkConnectedOverlayConfig { origin?: CdkOverlayOrigin | FlexibleConnectedPositionStrategyOrigin; @@ -283,9 +291,14 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { constructor() { const templateRef = inject>(TemplateRef); const viewContainerRef = inject(ViewContainerRef); + const defaultConfig = inject(CDK_CONNECTED_OVERLAY_DEFAULT_CONFIG, {optional: true}); this._templatePortal = new TemplatePortal(templateRef, viewContainerRef); this.scrollStrategy = this._scrollStrategyFactory(); + + if (defaultConfig) { + this._assignConfig(defaultConfig); + } } /** The associated overlay reference. */ diff --git a/src/cdk/overlay/public-api.ts b/src/cdk/overlay/public-api.ts index ee673e9facda..b76ed82361ae 100644 --- a/src/cdk/overlay/public-api.ts +++ b/src/cdk/overlay/public-api.ts @@ -17,6 +17,7 @@ export { CdkOverlayOrigin, CdkConnectedOverlay, CdkConnectedOverlayConfig, + CDK_CONNECTED_OVERLAY_DEFAULT_CONFIG, } from './overlay-directives'; export {FullscreenOverlayContainer} from './fullscreen-overlay-container'; export {OverlayRef, OverlaySizeConfig} from './overlay-ref';