From c0945db474e7e967342d180ce531851971e65d8e Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Tue, 14 Oct 2025 15:15:21 +0900 Subject: [PATCH 1/6] refactor(cdk/overlay): clarify usage of dimensions v.s. placement u --- .../flexible-connected-position-strategy.ts | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index fbc133659c0b..546a156a389f 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -42,8 +42,11 @@ export type FlexibleConnectedPositionStrategyOrigin = height?: number; }); -/** Equivalent of `DOMRect` without some of the properties we don't care about. */ -type Dimensions = Omit; +/** Refinement of `DOMRect` when only width/height and position (l, r, t, b) are needed. */ +type Rect = Omit; + +/** Further refinement of above for when only the dimensions are needed. */ +type Dimensions = Omit; /** * Creates a flexible position strategy. @@ -95,17 +98,17 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Whether the overlay position is locked. */ private _positionLocked = false; - /** Cached origin dimensions */ - private _originRect: Dimensions; + /** Cached origin placement and dimentsions. */ + private _originRect: Rect; - /** Cached overlay dimensions */ - private _overlayRect: Dimensions; + /** Cached overlay placement and dimensions */ + private _overlayRect: Rect; - /** Cached viewport dimensions */ - private _viewportRect: Dimensions; + /** Cached viewport placement and dimensions */ + private _viewportRect: Rect; - /** Cached container dimensions */ - private _containerRect: Dimensions; + /** Cached container placement and dimensions */ + private _containerRect: Rect; /** Amount of space that must be maintained between the overlay and the right edge of the viewport. */ private _viewportMargin: ViewportMargin = 0; @@ -514,11 +517,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** * Gets the (x, y) coordinate of a connection point on the origin based on a relative position. */ - private _getOriginPoint( - originRect: Dimensions, - containerRect: Dimensions, - pos: ConnectedPosition, - ): Point { + private _getOriginPoint(originRect: Rect, containerRect: Rect, pos: ConnectedPosition): Point { let x: number; if (pos.originX == 'center') { // Note: when centering we should always use the `left` @@ -592,7 +591,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Gets how well an overlay at the given point will fit within the viewport. */ private _getOverlayFit( point: Point, - rawOverlayRect: Dimensions, + rawOverlayRect: Rect, viewport: Dimensions, position: ConnectedPosition, ): OverlayFit { @@ -637,7 +636,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { * @param point The (x, y) coordinates of the overlay at some position. * @param viewport The geometry of the viewport. */ - private _canFitWithFlexibleDimensions(fit: OverlayFit, point: Point, viewport: Dimensions) { + private _canFitWithFlexibleDimensions(fit: OverlayFit, point: Point, viewport: Rect) { if (this._hasFlexibleDimensions) { const availableHeight = viewport.bottom - point.y; const availableWidth = viewport.right - point.x; @@ -667,7 +666,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { */ private _pushOverlayOnScreen( start: Point, - rawOverlayRect: Dimensions, + rawOverlayRect: Rect, scrollPosition: ViewportScrollPosition, ): Point { // If the position is locked and we've pushed the overlay already, reuse the previous push @@ -1113,7 +1112,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } /** Narrows the given viewport rect by the current _viewportMargin. */ - private _getNarrowedViewportRect(): Dimensions { + private _getNarrowedViewportRect(): Rect { // We recalculate the viewport rect here ourselves, rather than using the ViewportRuler, // because we want to use the `clientWidth` and `clientHeight` as the base. The difference // being that the client properties don't include the scrollbar, as opposed to `innerWidth` @@ -1231,7 +1230,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } /** Returns the DOMRect of the current origin. */ - private _getOriginRect(): Dimensions { + private _getOriginRect(): Rect { const origin = this._origin; if (origin instanceof ElementRef) { @@ -1353,7 +1352,7 @@ function getPixelValue(input: number | string | null | undefined): number | null * deviations in the `DOMRect` returned by the browser (e.g. when zoomed in with a percentage * size, see #21350). */ -function getRoundedBoundingClientRect(clientRect: Dimensions): Dimensions { +function getRoundedBoundingClientRect(clientRect: Rect): Rect { return { top: Math.floor(clientRect.top), right: Math.floor(clientRect.right), From aeb9dbf82dfc78b31297a0c1e7b970968870944d Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Tue, 14 Oct 2025 16:09:05 +0900 Subject: [PATCH 2/6] feat(cdk/overlay): Add option to insert overlay after an element instead of at root --- goldens/cdk/overlay/index.api.md | 4 ++- src/cdk/overlay/overlay-config.ts | 4 +++ src/cdk/overlay/overlay-directives.ts | 5 ++++ src/cdk/overlay/overlay.ts | 12 +++++++-- .../flexible-connected-position-strategy.ts | 25 ++++++++++++++++++- 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index d2537257ed04..2e462f52783a 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -54,6 +54,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { growAfterOpen: boolean; hasBackdrop: boolean; height: number | string; + insertOverlayAfter?: ElementRef; lockPosition: boolean; minHeight: number | string; minWidth: number | string; @@ -92,7 +93,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; } @@ -324,6 +325,7 @@ export class OverlayConfig { disposeOnNavigation?: boolean; hasBackdrop?: boolean; height?: number | string; + insertOverlayAfter?: ElementRef; maxHeight?: number | string; maxWidth?: number | string; minHeight?: number | string; diff --git a/src/cdk/overlay/overlay-config.ts b/src/cdk/overlay/overlay-config.ts index fa1d96551280..e003ad5343a6 100644 --- a/src/cdk/overlay/overlay-config.ts +++ b/src/cdk/overlay/overlay-config.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import {ElementRef} from '@angular/core'; import {PositionStrategy} from './position/position-strategy'; import {Direction, Directionality} from '../bidi'; import {ScrollStrategy, NoopScrollStrategy} from './scroll/index'; @@ -30,6 +31,9 @@ export class OverlayConfig { /** Whether to disable any built-in animations. */ disableAnimations?: boolean; + /** If specified, insert overlay after this element, instead of using the global overlay container. */ + insertOverlayAfter?: ElementRef; + /** The width of the overlay panel. If a number is provided, pixel units are assumed. */ width?: number | string; diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 5f4e711b7fc9..804341666652 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -209,6 +209,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { @Input({alias: 'cdkConnectedOverlayGrowAfterOpen', transform: booleanAttribute}) growAfterOpen: boolean = false; + /** Whether or not the overlay should attach a backdrop. */ + @Input('cdkConnectedOverlayInsertAfter') + insertOverlayAfter?: ElementRef; + /** Whether the overlay can be pushed on-screen if none of the provided positions fit. */ @Input({alias: 'cdkConnectedOverlayPush', transform: booleanAttribute}) push: boolean = false; @@ -327,6 +331,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { scrollStrategy: this.scrollStrategy, hasBackdrop: this.hasBackdrop, disposeOnNavigation: this.disposeOnNavigation, + insertOverlayAfter: this.insertOverlayAfter, }); if (this.width || this.width === 0) { diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index 935196bd2c3e..e661d4f2f06a 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -48,13 +48,21 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov const appRef = injector.get(ApplicationRef); const directionality = injector.get(Directionality); + // Create the overlay pane and a parent which will then be attached to the document. const host = doc.createElement('div'); const pane = doc.createElement('div'); - pane.id = idGenerator.getId('cdk-overlay-'); pane.classList.add('cdk-overlay-pane'); host.appendChild(pane); - overlayContainer.getContainerElement().appendChild(host); + + // Insert after the specified element, or onto the global overlay container. + if (config?.insertOverlayAfter) { + const element = config.insertOverlayAfter.nativeElement; + element.after(host); + element.parentElement.style.position = 'relative'; + } else { + overlayContainer.getContainerElement().appendChild(host); + } const portalOutlet = new DomPortalOutlet(pane, appRef, injector); const overlayConfig = new OverlayConfig(config); diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 546a156a389f..81f66b3a8553 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -581,6 +581,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { overlayStartY = pos.overlayY == 'top' ? 0 : -overlayRect.height; } + // Adjust the overly position when it is placed inline relative to its parent. + const insertOverlayAfter = this._overlayRef.getConfig().insertOverlayAfter; + if (insertOverlayAfter) { + const rect = insertOverlayAfter!.nativeElement.parentElement.getBoundingClientRect(); + overlayStartX -= rect.left; + overlayStartY -= rect.top; + } + // The (x, y) coordinates of the overlay. return { x: originPoint.x + overlayStartX, @@ -889,11 +897,26 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { if (this._hasExactPosition()) { styles.top = styles.left = '0'; styles.bottom = styles.right = styles.maxHeight = styles.maxWidth = ''; - styles.width = styles.height = '100%'; + + if (this._overlayRef.getConfig().insertOverlayAfter) { + styles.width = coerceCssPixelValue(boundingBoxRect.width); + styles.height = coerceCssPixelValue(boundingBoxRect.height); + } else { + // TODO(andreyd): can most likley remove this for common case + styles.width = styles.height = '100%'; + } } else { const maxHeight = this._overlayRef.getConfig().maxHeight; const maxWidth = this._overlayRef.getConfig().maxWidth; + // Adjust the overly position when it is placed inline relative to its parent. + const insertOverlayAfter = this._overlayRef.getConfig().insertOverlayAfter; + if (insertOverlayAfter) { + const rect = insertOverlayAfter!.nativeElement.parentElement.getBoundingClientRect(); + boundingBoxRect.left -= rect.left; + boundingBoxRect.top -= rect.top; + } + styles.height = coerceCssPixelValue(boundingBoxRect.height); styles.top = coerceCssPixelValue(boundingBoxRect.top); styles.bottom = coerceCssPixelValue(boundingBoxRect.bottom); From 2f3de9199890866d2551c3fac0793f5754330058 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Thu, 16 Oct 2025 23:58:40 +0900 Subject: [PATCH 3/6] feat(cdk/menu): add option to enable inlined overlay --- goldens/cdk/menu/index.api.md | 5 ++++- src/cdk/menu/menu-trigger-base.ts | 5 +++++ src/cdk/menu/menu-trigger.ts | 3 +++ .../cdk-menu-standalone-menu-example.html | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/goldens/cdk/menu/index.api.md b/goldens/cdk/menu/index.api.md index 27be93023ab9..85e060f5e732 100644 --- a/goldens/cdk/menu/index.api.md +++ b/goldens/cdk/menu/index.api.md @@ -216,6 +216,8 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD getMenu(): Menu | undefined; _handleClick(): void; // (undocumented) + static ngAcceptInputType_menuOverlayInlined: unknown; + // (undocumented) ngOnChanges(changes: SimpleChanges): void; // (undocumented) ngOnDestroy(): void; @@ -224,7 +226,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD toggle(): void; _toggleOnKeydown(event: KeyboardEvent): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -240,6 +242,7 @@ export abstract class CdkMenuTriggerBase implements OnDestroy { protected isElementInsideMenuStack(element: Element): boolean; isOpen(): boolean; menuData: unknown; + menuOverlayInlined: boolean; menuPosition: ConnectedPosition[]; protected readonly menuScrollStrategy: () => ScrollStrategy; protected readonly menuStack: MenuStack; diff --git a/src/cdk/menu/menu-trigger-base.ts b/src/cdk/menu/menu-trigger-base.ts index 7656603cc00a..46008dd2093e 100644 --- a/src/cdk/menu/menu-trigger-base.ts +++ b/src/cdk/menu/menu-trigger-base.ts @@ -90,6 +90,11 @@ export abstract class CdkMenuTriggerBase implements OnDestroy { */ menuPosition: ConnectedPosition[]; + /** + * Whether to inline the overlay, instead of using the global overlay container. + */ + menuOverlayInlined: boolean; + /** Emits when the attached menu is requested to open */ readonly opened: EventEmitter = new EventEmitter(); diff --git a/src/cdk/menu/menu-trigger.ts b/src/cdk/menu/menu-trigger.ts index d37ab81a4205..0671f46821db 100644 --- a/src/cdk/menu/menu-trigger.ts +++ b/src/cdk/menu/menu-trigger.ts @@ -17,6 +17,7 @@ import { OnDestroy, Renderer2, SimpleChanges, + booleanAttribute, } from '@angular/core'; import {InputModalityDetector} from '../a11y'; import {Directionality} from '../bidi'; @@ -68,6 +69,7 @@ import {eventDispatchesNativeClick} from './event-detection'; inputs: [ {name: 'menuTemplateRef', alias: 'cdkMenuTriggerFor'}, {name: 'menuPosition', alias: 'cdkMenuPosition'}, + {name: 'menuOverlayInlined', alias: 'cdkMenuOverlayInlined', transform: booleanAttribute}, {name: 'menuData', alias: 'cdkMenuTriggerData'}, ], outputs: ['opened: cdkMenuOpened', 'closed: cdkMenuClosed'], @@ -276,6 +278,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnChanges, OnD positionStrategy: this._getOverlayPositionStrategy(), scrollStrategy: this.menuScrollStrategy(), direction: this._directionality || undefined, + insertOverlayAfter: this.menuOverlayInlined ? this._elementRef : undefined, }); } diff --git a/src/components-examples/cdk/menu/cdk-menu-standalone-menu/cdk-menu-standalone-menu-example.html b/src/components-examples/cdk/menu/cdk-menu-standalone-menu/cdk-menu-standalone-menu-example.html index dba52a3f3377..4c0efaffe99d 100644 --- a/src/components-examples/cdk/menu/cdk-menu-standalone-menu/cdk-menu-standalone-menu-example.html +++ b/src/components-examples/cdk/menu/cdk-menu-standalone-menu/cdk-menu-standalone-menu-example.html @@ -1,5 +1,5 @@ - + From 77b05f7a42ed543ab9135f90769c7d9794127ebc Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Thu, 16 Oct 2025 23:11:24 +0900 Subject: [PATCH 4/6] feat(material/autocomplete): add option to enable inlined overlay --- goldens/material/autocomplete/index.api.md | 5 ++++- src/dev-app/autocomplete/autocomplete-demo.html | 4 +++- src/dev-app/autocomplete/autocomplete-demo.ts | 4 +++- src/material/autocomplete/autocomplete-trigger.ts | 7 +++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/goldens/material/autocomplete/index.api.md b/goldens/material/autocomplete/index.api.md index d1d794adcb43..f4147471d4cf 100644 --- a/goldens/material/autocomplete/index.api.md +++ b/goldens/material/autocomplete/index.api.md @@ -174,6 +174,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn // (undocumented) static ngAcceptInputType_autocompleteDisabled: unknown; // (undocumented) + static ngAcceptInputType_overlayInlined: unknown; + // (undocumented) ngAfterViewInit(): void; // (undocumented) ngOnChanges(changes: SimpleChanges): void; @@ -183,6 +185,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn _onTouched: () => void; openPanel(): void; readonly optionSelections: Observable; + overlayInlined: boolean; readonly _overlayPanelClass: string[]; get panelClosingActions(): Observable; get panelOpen(): boolean; @@ -197,7 +200,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn // (undocumented) writeValue(value: any): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } diff --git a/src/dev-app/autocomplete/autocomplete-demo.html b/src/dev-app/autocomplete/autocomplete-demo.html index 401f1180e4ae..3f97f4d7578a 100644 --- a/src/dev-app/autocomplete/autocomplete-demo.html +++ b/src/dev-app/autocomplete/autocomplete-demo.html @@ -12,6 +12,7 @@ #reactiveInput matInput [matAutocomplete]="reactiveAuto" + [matAutocompleteOverlayInlined]="true" [formControl]="stateCtrl" (input)="reactiveStates = filterStates(reactiveInput.value)" (focus)="reactiveStates = filterStates(reactiveInput.value)"> @@ -71,7 +72,7 @@ @if (true) { State - diff --git a/src/dev-app/autocomplete/autocomplete-demo.ts b/src/dev-app/autocomplete/autocomplete-demo.ts index 408217b7679e..22769cb7c78a 100644 --- a/src/dev-app/autocomplete/autocomplete-demo.ts +++ b/src/dev-app/autocomplete/autocomplete-demo.ts @@ -221,7 +221,9 @@ export class AutocompleteDemo {

Choose a T-shirt size.

T-Shirt Size - + @for (size of sizes; track size) { {{size}} diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index 486c7a60728a..4e67c1cfa38b 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -224,6 +224,12 @@ export class MatAutocompleteTrigger */ @Input('matAutocompletePosition') position: 'auto' | 'above' | 'below' = 'auto'; + /** + * Whether to inline the overlay, instead of using the global overlay container. + */ + @Input({alias: 'matAutocompleteOverlayInlined', transform: booleanAttribute}) + overlayInlined: boolean; + /** * Reference relative to which to position the autocomplete panel. * Defaults to the autocomplete trigger element. @@ -894,6 +900,7 @@ export class MatAutocompleteTrigger backdropClass: this._defaults?.backdropClass || 'cdk-overlay-transparent-backdrop', panelClass: this._overlayPanelClass, disableAnimations: this._animationsDisabled, + insertOverlayAfter: this.overlayInlined ? this._getConnectedElement() : undefined, }); } From 8173e0ae140a624b92f6e6a932a242bde805969e Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Fri, 17 Oct 2025 00:32:35 +0900 Subject: [PATCH 5/6] feat(material/menu): add option to enable inlined overlay --- goldens/material/menu/index.api.md | 6 +++++- src/dev-app/menu/menu-demo.html | 2 +- src/material/menu/menu-trigger-base.ts | 6 ++++++ src/material/menu/menu-trigger.ts | 12 ++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/goldens/material/menu/index.api.md b/goldens/material/menu/index.api.md index 2b79df3d1af9..8e8bf25f478e 100644 --- a/goldens/material/menu/index.api.md +++ b/goldens/material/menu/index.api.md @@ -274,6 +274,10 @@ export class MatMenuTrigger extends MatMenuTriggerBase implements AfterContentIn readonly menuClosed: EventEmitter; menuData: any; readonly menuOpened: EventEmitter; + get menuOverlayInlined(): boolean; + set menuOverlayInlined(menuOverlayInlined: boolean); + // (undocumented) + static ngAcceptInputType_menuOverlayInlined: unknown; // (undocumented) ngAfterContentInit(): void; // (undocumented) @@ -288,7 +292,7 @@ export class MatMenuTrigger extends MatMenuTriggerBase implements AfterContentIn triggersSubmenu(): boolean; updatePosition(): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } diff --git a/src/dev-app/menu/menu-demo.html b/src/dev-app/menu/menu-demo.html index d4cc3801ed01..caa1d77daf26 100644 --- a/src/dev-app/menu/menu-demo.html +++ b/src/dev-app/menu/menu-demo.html @@ -3,7 +3,7 @@

You clicked on: {{ selected }}

- diff --git a/src/material/menu/menu-trigger-base.ts b/src/material/menu/menu-trigger-base.ts index 32006cd59702..b87a2474e2d0 100644 --- a/src/material/menu/menu-trigger-base.ts +++ b/src/material/menu/menu-trigger-base.ts @@ -104,6 +104,11 @@ export abstract class MatMenuTriggerBase implements OnDestroy { /** Data that will be passed to the menu panel. */ abstract menuData: any; + /** + * Whether to inline the overlay, instead of using the global overlay container. + */ + protected _menuOverlayInlined: boolean; + /** Whether focus should be restored when the menu is closed. */ abstract restoreFocus: boolean; @@ -367,6 +372,7 @@ export abstract class MatMenuTriggerBase implements OnDestroy { scrollStrategy: this._scrollStrategy(), direction: this._dir || 'ltr', disableAnimations: this._animationsDisabled, + insertOverlayAfter: this._menuOverlayInlined ? this._element : undefined, }); } diff --git a/src/material/menu/menu-trigger.ts b/src/material/menu/menu-trigger.ts index 7d4cb842557a..455620043d56 100644 --- a/src/material/menu/menu-trigger.ts +++ b/src/material/menu/menu-trigger.ts @@ -17,6 +17,7 @@ import { OnDestroy, Output, Renderer2, + booleanAttribute, } from '@angular/core'; import {OverlayRef} from '@angular/cdk/overlay'; import {Subscription} from 'rxjs'; @@ -67,6 +68,17 @@ export class MatMenuTrigger extends MatMenuTriggerBase implements AfterContentIn @Input('matMenuTriggerData') override menuData: any; + /** + * Whether to inline the overlay, instead of using the global overlay container. + */ + @Input({alias: 'matMenuTriggerOverlayInlined', transform: booleanAttribute}) + get menuOverlayInlined(): boolean { + return this._menuOverlayInlined; + } + set menuOverlayInlined(menuOverlayInlined: boolean) { + this._menuOverlayInlined = menuOverlayInlined; + } + /** * Whether focus should be restored when the menu is closed. * Note that disabling this option can have accessibility implications From a49969fb1f3429f09d1644e787b053eb9febedab Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Mon, 20 Oct 2025 16:05:45 -0700 Subject: [PATCH 6/6] feat(material/select): add option to enable inlined overlay --- goldens/material/paginator/index.api.md | 1 - goldens/material/select/index.api.md | 9 ++++++--- src/dev-app/select/select-demo.html | 23 ++++++++++++----------- src/material/select/select.html | 1 + src/material/select/select.ts | 13 ++++++++++++- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/goldens/material/paginator/index.api.md b/goldens/material/paginator/index.api.md index 5ab41274a447..9e1810f7a686 100644 --- a/goldens/material/paginator/index.api.md +++ b/goldens/material/paginator/index.api.md @@ -13,7 +13,6 @@ import { AfterViewChecked } from '@angular/core'; import { AfterViewInit } from '@angular/core'; import { BooleanInput } from '@angular/cdk/coercion'; import { CdkConnectedOverlay } from '@angular/cdk/overlay'; -import { CdkOverlayOrigin } from '@angular/cdk/overlay'; import { ChangeDetectorRef } from '@angular/core'; import { ConnectedPosition } from '@angular/cdk/overlay'; import { ControlValueAccessor } from '@angular/forms'; diff --git a/goldens/material/select/index.api.md b/goldens/material/select/index.api.md index 2f02eff60d41..37b125e36a79 100644 --- a/goldens/material/select/index.api.md +++ b/goldens/material/select/index.api.md @@ -13,7 +13,6 @@ import { AfterViewChecked } from '@angular/core'; import { AfterViewInit } from '@angular/core'; import { BooleanInput } from '@angular/cdk/coercion'; import { CdkConnectedOverlay } from '@angular/cdk/overlay'; -import { CdkOverlayOrigin } from '@angular/cdk/overlay'; import { ChangeDetectorRef } from '@angular/core'; import { ConnectedPosition } from '@angular/cdk/overlay'; import { ControlValueAccessor } from '@angular/forms'; @@ -280,6 +279,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit set hideSingleSelectionIndicator(value: boolean); get id(): string; set id(value: string); + get inlineOverlayAfter(): ElementRef | undefined; _isRtl(): boolean; _keyManager: ActiveDescendantKeyManager; get multiple(): boolean; @@ -297,6 +297,8 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit // (undocumented) static ngAcceptInputType_multiple: unknown; // (undocumented) + static ngAcceptInputType_overlayInlined: unknown; + // (undocumented) static ngAcceptInputType_required: unknown; // (undocumented) static ngAcceptInputType_tabIndex: unknown; @@ -327,6 +329,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit options: QueryList; readonly optionSelectionChanges: Observable; protected _overlayDir: CdkConnectedOverlay; + overlayInlined: boolean; // (undocumented) _overlayPanelClass: string | string[]; _overlayWidth: string | number; @@ -341,7 +344,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit get placeholder(): string; set placeholder(value: string); _positions: ConnectedPosition[]; - _preferredOverlayOrigin: CdkOverlayOrigin | ElementRef | undefined; + _preferredOverlayOrigin: ElementRef | undefined; registerOnChange(fn: (value: any) => void): void; registerOnTouched(fn: () => {}): void; get required(): boolean; @@ -372,7 +375,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit protected _viewportRuler: ViewportRuler; writeValue(value: any): void; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } diff --git a/src/dev-app/select/select-demo.html b/src/dev-app/select/select-demo.html index 8742db08db36..410636a9fdfc 100644 --- a/src/dev-app/select/select-demo.html +++ b/src/dev-app/select/select-demo.html @@ -11,7 +11,7 @@ [class.demo-drinks-width-large]="drinksWidth === '400px'"> Drink + [disabled]="drinksDisabled" overlayInlined #drinkControl="ngModel"> None @for (drink of drinks; track drink; let index = $index) { Pokemon - @for (creature of pokemon; track creature) { @@ -108,7 +108,7 @@ Digimon - + None @for (creature of digimon; track creature) { {{ creature.viewValue }} @@ -129,7 +129,7 @@ Pokemon - + @for (group of pokemonGroups; track group) { @for (creature of group.pokemon; track creature) { @@ -151,6 +151,7 @@ @for (drink of drinks; track drink) { {{ drink.viewValue }} @@ -179,7 +180,7 @@

Fill - + None @for (creature of digimon; track creature) { {{ creature.viewValue }} @@ -191,7 +192,7 @@

Outline - + None @for (creature of digimon; track creature) { {{ creature.viewValue }} @@ -212,7 +213,7 @@ Food I would like to eat - + @for (food of foods; track food) { {{ food.viewValue }} } @@ -238,7 +239,7 @@ Starter pokemon - + @for (creature of pokemon; track creature) { {{ creature.viewValue }} } @@ -387,7 +388,7 @@

Error message with errorStateMatcher

Bread - @for (bread of breads; track bread) { {{ bread.viewValue }} @@ -396,7 +397,7 @@

Error message with errorStateMatcher

Meat - @for (meat of meats; track meat) { {{ meat.viewValue }} @@ -405,7 +406,7 @@

Error message with errorStateMatcher

Cheese - @for (cheese of cheeses; track cheese) { {{ cheese.viewValue }} diff --git a/src/material/select/select.html b/src/material/select/select.html index 6eaecee7f5c3..27756ff148d0 100644 --- a/src/material/select/select.html +++ b/src/material/select/select.html @@ -40,6 +40,7 @@ [cdkConnectedOverlayPositions]="_positions" [cdkConnectedOverlayWidth]="_overlayWidth" [cdkConnectedOverlayFlexibleDimensions]="true" + [cdkConnectedOverlayInsertAfter]="inlineOverlayAfter" (detach)="close()" (backdropClick)="close()" (overlayKeydown)="_handleOverlayKeydown($event)"> diff --git a/src/material/select/select.ts b/src/material/select/select.ts index b05f7f8b6e3a..0396f85c5736 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -346,7 +346,7 @@ export class MatSelect _keyManager: ActiveDescendantKeyManager; /** Ideal origin for the overlay panel. */ - _preferredOverlayOrigin: CdkOverlayOrigin | ElementRef | undefined; + _preferredOverlayOrigin: ElementRef | undefined; /** Width of the overlay panel. */ _overlayWidth: string | number; @@ -555,6 +555,12 @@ export class MatSelect @Input({transform: booleanAttribute}) canSelectNullableOptions: boolean = this._defaultOptions?.canSelectNullableOptions ?? false; + /** + * Whether to inline the overlay, instead of using the global overlay container. + */ + @Input({transform: booleanAttribute}) + overlayInlined: boolean; + /** Combined stream of all of the child options' change events. */ readonly optionSelectionChanges: Observable = defer(() => { const options = this.options; @@ -948,6 +954,11 @@ export class MatSelect return this._selectionModel.selected[0].viewValue; } + /** Whether to inline the overlay and after which element. */ + get inlineOverlayAfter(): ElementRef | undefined { + return this.overlayInlined ? this._parentFormField?.getConnectedOverlayOrigin() : undefined; + } + /** Refreshes the error state of the select. */ updateErrorState() { this._errorStateTracker.updateErrorState();