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/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.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 fbc133659c0b..a5a37f477be9 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` @@ -582,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.getBoundingClientRect(); + overlayStartX -= rect.left; + overlayStartY -= rect.top; + } + // The (x, y) coordinates of the overlay. return { x: originPoint.x + overlayStartX, @@ -592,7 +599,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 +644,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 +674,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 @@ -890,7 +897,14 @@ 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; @@ -1113,7 +1127,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 +1245,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 +1367,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), 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 @@ - + 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..e2f36d55e823 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/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/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, }); } 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