From 1dea7340628bea4ab08e9b53511ef001ce0fc0ca Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 4 Nov 2025 14:03:47 +0100 Subject: [PATCH 1/4] refactor(cdk/overlay): tweak popover resets Updates the popover resets based on recent tests. --- src/cdk/overlay/_index.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cdk/overlay/_index.scss b/src/cdk/overlay/_index.scss index 50c929a0665d..714cf2979858 100644 --- a/src/cdk/overlay/_index.scss +++ b/src/cdk/overlay/_index.scss @@ -190,11 +190,13 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default; background: none; border: none; padding: 0; - color: inherit; outline: 0; overflow: visible; position: fixed; pointer-events: none; + white-space: normal; + line-height: normal; + text-decoration: none; // These are important so the overlay can be measured before it's fully inserted. width: 100%; From be45ee216e360780054fcffbb7b2185ba9f6ba76 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 4 Nov 2025 14:10:58 +0100 Subject: [PATCH 2/4] refactor(cdk/overlay): keep global overlays inside the container Keeps global overlays inside the overlay container since it can be breaking for users to move them out. --- goldens/cdk/overlay/index.api.md | 6 +-- src/cdk/overlay/overlay-ref.ts | 8 +++- src/cdk/overlay/overlay.ts | 8 +++- .../position/global-position-strategy.spec.ts | 46 ------------------- .../position/global-position-strategy.ts | 18 ++------ src/cdk/overlay/position/position-strategy.ts | 2 +- 6 files changed, 18 insertions(+), 70 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index f3d7a5f9ff45..b042d4658346 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -267,7 +267,7 @@ export function createCloseScrollStrategy(injector: Injector, config?: CloseScro export function createFlexibleConnectedPositionStrategy(injector: Injector, origin: FlexibleConnectedPositionStrategyOrigin): FlexibleConnectedPositionStrategy; // @public -export function createGlobalPositionStrategy(injector: Injector): GlobalPositionStrategy; +export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy; // @public export function createNoopScrollStrategy(): NoopScrollStrategy; @@ -327,7 +327,6 @@ export class FullscreenOverlayContainer extends OverlayContainer implements OnDe // @public export class GlobalPositionStrategy implements PositionStrategy { - constructor(injector?: Injector); apply(): void; // (undocumented) attach(overlayRef: OverlayRef): void; @@ -336,7 +335,6 @@ export class GlobalPositionStrategy implements PositionStrategy { centerVertically(offset?: string): this; dispose(): void; end(value?: string): this; - getPopoverInsertionPoint(): Element; // @deprecated height(value?: string): this; left(value?: string): this; @@ -525,7 +523,7 @@ export interface PositionStrategy { attach(overlayRef: OverlayRef): void; detach?(): void; dispose(): void; - getPopoverInsertionPoint?(): Element; + getPopoverInsertionPoint?(): Element | null; } // @public diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index c78012a5c9c8..5d4792606890 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -405,8 +405,12 @@ export class OverlayRef implements PortalOutlet { private _attachHost() { if (!this._host.parentElement) { - if (this._config.usePopover && this._positionStrategy?.getPopoverInsertionPoint) { - this._positionStrategy.getPopoverInsertionPoint().after(this._host); + const customInsertionPoint = this._config.usePopover + ? this._positionStrategy?.getPopoverInsertionPoint?.() + : null; + + if (customInsertionPoint) { + customInsertionPoint.after(this._host); } else { this._previousHostParent?.appendChild(this._host); } diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index 1e3f059a0ddd..6b739a0a57a4 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -67,8 +67,12 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov host.classList.add('cdk-overlay-popover'); } - if (overlayConfig.usePopover && overlayConfig.positionStrategy?.getPopoverInsertionPoint) { - overlayConfig.positionStrategy.getPopoverInsertionPoint().after(host); + const customInsertionPoint = overlayConfig.usePopover + ? overlayConfig.positionStrategy?.getPopoverInsertionPoint?.() + : null; + + if (customInsertionPoint) { + customInsertionPoint.after(host); } else { overlayContainer.getContainerElement().appendChild(host); } diff --git a/src/cdk/overlay/position/global-position-strategy.spec.ts b/src/cdk/overlay/position/global-position-strategy.spec.ts index 550b12c271f5..9d83675b063f 100644 --- a/src/cdk/overlay/position/global-position-strategy.spec.ts +++ b/src/cdk/overlay/position/global-position-strategy.spec.ts @@ -7,8 +7,6 @@ import { OverlayRef, createOverlayRef, createGlobalPositionStrategy, - GlobalPositionStrategy, - OverlayContainer, } from '../index'; describe('GlobalPositonStrategy', () => { @@ -472,50 +470,6 @@ describe('GlobalPositonStrategy', () => { expect(elementStyle.marginRight).toBe(''); expect(parentStyle.justifyContent).toBe('flex-end'); }); - - describe('DOM location', () => { - let positionStrategy: GlobalPositionStrategy; - let containerElement: HTMLElement; - - beforeEach(() => { - containerElement = TestBed.inject(OverlayContainer).getContainerElement(); - positionStrategy = createGlobalPositionStrategy(injector); - }); - - it('should place the overlay inside the overlay container by default', () => { - attachOverlay({positionStrategy, usePopover: false}); - expect(containerElement.contains(overlayRef.hostElement)).toBe(true); - expect(overlayRef.hostElement.getAttribute('popover')).toBeFalsy(); - }); - - it('should be able to opt into placing the overlay inside a popover element', () => { - if (!('showPopover' in document.body)) { - return; - } - - attachOverlay({positionStrategy, usePopover: true}); - - expect(containerElement.contains(overlayRef.hostElement)).toBe(false); - expect(document.body.lastChild).toBe(overlayRef.hostElement); - expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual'); - }); - - it('should re-attach the popover at the end of the body', () => { - if (!('showPopover' in document.body)) { - return; - } - - attachOverlay({positionStrategy, usePopover: true}); - expect(document.body.lastChild).toBe(overlayRef.hostElement); - - overlayRef.detach(); - TestBed.inject(ApplicationRef).tick(); - expect(overlayRef.hostElement.parentNode).toBeFalsy(); - - overlayRef.attach(portal); - expect(document.body.lastChild).toBe(overlayRef.hostElement); - }); - }); }); @Component({ diff --git a/src/cdk/overlay/position/global-position-strategy.ts b/src/cdk/overlay/position/global-position-strategy.ts index a29af9b31651..2c0f11dabcc4 100644 --- a/src/cdk/overlay/position/global-position-strategy.ts +++ b/src/cdk/overlay/position/global-position-strategy.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {DOCUMENT, Injector} from '@angular/core'; +import {Injector} from '@angular/core'; import {OverlayRef} from '../overlay-ref'; import {PositionStrategy} from './position-strategy'; @@ -17,8 +17,8 @@ const wrapperClass = 'cdk-global-overlay-wrapper'; * Creates a global position strategy. * @param injector Injector used to resolve dependencies for the strategy. */ -export function createGlobalPositionStrategy(injector: Injector): GlobalPositionStrategy { - return new GlobalPositionStrategy(injector); +export function createGlobalPositionStrategy(_injector: Injector): GlobalPositionStrategy { + return new GlobalPositionStrategy(); } /** @@ -39,13 +39,6 @@ export class GlobalPositionStrategy implements PositionStrategy { private _width = ''; private _height = ''; private _isDisposed = false; - private _document: Document; - - constructor(injector?: Injector) { - // TODO(crisbeto): injector should be required, but some internal apps - // don't go through `createGlobalPositionStrategy` so they don't provide it. - this._document = injector?.get(DOCUMENT) || document; - } attach(overlayRef: OverlayRef): void { const config = overlayRef.getConfig(); @@ -274,9 +267,4 @@ export class GlobalPositionStrategy implements PositionStrategy { this._overlayRef = null!; this._isDisposed = true; } - - /** @docs-private */ - getPopoverInsertionPoint(): Element { - return this._document.body.lastChild as Element; - } } diff --git a/src/cdk/overlay/position/position-strategy.ts b/src/cdk/overlay/position/position-strategy.ts index de0e96626502..1191acb2dc0e 100644 --- a/src/cdk/overlay/position/position-strategy.ts +++ b/src/cdk/overlay/position/position-strategy.ts @@ -26,5 +26,5 @@ export interface PositionStrategy { * Gets the element in the DOM after which to insert * the overlay when it is rendered out as a popover. */ - getPopoverInsertionPoint?(): Element; + getPopoverInsertionPoint?(): Element | null; } From 946b99717e3dec2e8748f40cf609ad05efadb246 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 4 Nov 2025 19:40:55 +0100 Subject: [PATCH 3/4] refactor(cdk/overlay): allow connected overlays to stay in the overlay container Adds an API that allows connected overlays to decide if they should be inline or in the overlay container when they're rendered out as popovers. --- goldens/cdk/overlay/index.api.md | 12 ++++++---- src/cdk/overlay/overlay-directives.ts | 12 ++++++---- ...exible-connected-position-strategy.spec.ts | 21 ++++++++-------- .../flexible-connected-position-strategy.ts | 24 ++++++++++++++++++- src/cdk/overlay/public-api.ts | 1 + 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index b042d4658346..bfca45f81adf 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -77,8 +77,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { // (undocumented) static ngAcceptInputType_push: unknown; // (undocumented) - static ngAcceptInputType_usePopover: unknown; - // (undocumented) ngOnChanges(changes: SimpleChanges): void; // (undocumented) ngOnDestroy(): void; @@ -98,7 +96,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { push: boolean; scrollStrategy: ScrollStrategy; transformOriginSelector: string; - usePopover: boolean; + usePopover: FlexibleOverlayPopoverLocation | null; viewportMargin: ViewportMargin; width: number | string; // (undocumented) @@ -150,7 +148,7 @@ export interface CdkConnectedOverlayConfig { // (undocumented) transformOriginSelector?: string; // (undocumented) - usePopover?: boolean; + usePopover?: FlexibleOverlayPopoverLocation | null; // (undocumented) viewportMargin?: ViewportMargin; // (undocumented) @@ -286,7 +284,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // (undocumented) detach(): void; dispose(): void; - getPopoverInsertionPoint(): Element; + getPopoverInsertionPoint(): Element | null; _origin: FlexibleConnectedPositionStrategyOrigin; positionChanges: Observable; get positions(): ConnectionPositionPair[]; @@ -298,6 +296,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { withFlexibleDimensions(flexibleDimensions?: boolean): this; withGrowAfterOpen(growAfterOpen?: boolean): this; withLockedPosition(isLocked?: boolean): this; + withPopoverLocation(location: FlexibleOverlayPopoverLocation): this; withPositions(positions: ConnectedPosition[]): this; withPush(canPush?: boolean): this; withScrollableContainers(scrollables: CdkScrollable[]): this; @@ -311,6 +310,9 @@ export type FlexibleConnectedPositionStrategyOrigin = ElementRef | Element | (Po height?: number; }); +// @public +export type FlexibleOverlayPopoverLocation = 'global' | 'inline'; + // @public export class FullscreenOverlayContainer extends OverlayContainer implements OnDestroy { constructor(...args: unknown[]); diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index 83ccf2a008c9..15bb05889834 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -38,6 +38,7 @@ import { createFlexibleConnectedPositionStrategy, FlexibleConnectedPositionStrategy, FlexibleConnectedPositionStrategyOrigin, + FlexibleOverlayPopoverLocation, } from './position/flexible-connected-position-strategy'; import {createRepositionScrollStrategy, ScrollStrategy} from './scroll/index'; @@ -127,7 +128,7 @@ export interface CdkConnectedOverlayConfig { growAfterOpen?: boolean; push?: boolean; disposeOnNavigation?: boolean; - usePopover?: boolean; + usePopover?: FlexibleOverlayPopoverLocation | null; matchWidth?: boolean; } @@ -251,8 +252,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { 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; + @Input({alias: 'cdkConnectedOverlayUsePopover'}) + usePopover: FlexibleOverlayPopoverLocation | null = null; /** Whether the overlay should match the trigger's width. */ @Input({alias: 'cdkConnectedOverlayMatchWidth', transform: booleanAttribute}) @@ -377,7 +378,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { scrollStrategy: this.scrollStrategy, hasBackdrop: this.hasBackdrop, disposeOnNavigation: this.disposeOnNavigation, - usePopover: this.usePopover, + usePopover: !!this.usePopover, }); if (this.height || this.height === 0) { @@ -423,7 +424,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { .withGrowAfterOpen(this.growAfterOpen) .withViewportMargin(this.viewportMargin) .withLockedPosition(this.lockPosition) - .withTransformOriginOn(this.transformOriginSelector); + .withTransformOriginOn(this.transformOriginSelector) + .withPopoverLocation(this.usePopover === 'global' ? 'global' : 'inline'); } /** Returns the position strategy of the overlay to be set on the overlay config */ diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index bfb472aff4d6..4a49f6b30f2b 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -2964,17 +2964,16 @@ describe('FlexibleConnectedPositionStrategy', () => { originElement = createPositionedBlockElement(); document.body.appendChild(originElement); - positionStrategy = createFlexibleConnectedPositionStrategy( - injector, - originElement, - ).withPositions([ - { - overlayX: 'start', - overlayY: 'top', - originX: 'start', - originY: 'bottom', - }, - ]); + positionStrategy = createFlexibleConnectedPositionStrategy(injector, originElement) + .withPopoverLocation('inline') + .withPositions([ + { + overlayX: 'start', + overlayY: 'top', + originX: 'start', + originY: 'bottom', + }, + ]); }); afterEach(() => { diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 7816b5f0c86b..8381a415d568 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -63,6 +63,9 @@ export function createFlexibleConnectedPositionStrategy( ); } +/** Supported locations in the DOM for connected overlays. */ +export type FlexibleOverlayPopoverLocation = 'global' | 'inline'; + /** * A strategy for positioning overlays. Using this strategy, an overlay is given an * implicit position relative some origin element. The relative position is defined in terms of @@ -158,6 +161,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Amount by which the overlay was pushed in each axis during the last time it was positioned. */ private _previousPushAmount: {x: number; y: number} | null; + /** Configures where in the DOM to insert the overlay when popovers are enabled. */ + private _popoverLocation: FlexibleOverlayPopoverLocation = 'global'; + /** Observable sequence of position changes. */ positionChanges: Observable = this._positionChanges; @@ -511,8 +517,24 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return this; } + /** + * Determines where in the DOM the overlay will be rendered when popover mode is enabled. + * @param location Configures the location in the DOM. Supports the following values: + * - `global` - The default which inserts the overlay inside the overlay container. + * - `inline` - Inserts the overlay next to the trigger. + */ + withPopoverLocation(location: FlexibleOverlayPopoverLocation): this { + this._popoverLocation = location; + return this; + } + /** @docs-private */ - getPopoverInsertionPoint(): Element { + getPopoverInsertionPoint(): Element | null { + // Return null so it falls back to inserting into the overlay container. + if (this._popoverLocation === 'global') { + return null; + } + const origin = this._origin; if (origin instanceof ElementRef) { diff --git a/src/cdk/overlay/public-api.ts b/src/cdk/overlay/public-api.ts index b76ed82361ae..729dee69c465 100644 --- a/src/cdk/overlay/public-api.ts +++ b/src/cdk/overlay/public-api.ts @@ -32,6 +32,7 @@ export { createGlobalPositionStrategy, } from './position/global-position-strategy'; export { + FlexibleOverlayPopoverLocation, ConnectedPosition, FlexibleConnectedPositionStrategy, FlexibleConnectedPositionStrategyOrigin, From 8f78acb7ae397b772af35a465955925fface4ea7 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 4 Nov 2025 19:40:58 +0100 Subject: [PATCH 4/4] refactor(material/tooltip): render tooltips as popovers Tests out rendering tooltips as popovers inside the overlay container. --- src/material/tooltip/tooltip.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index 7245a9bd7abf..a3c6f63751f0 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -508,7 +508,8 @@ export class MatTooltip implements OnDestroy, AfterViewInit { .withTransformOriginOn(`.${this._cssClassPrefix}-tooltip`) .withFlexibleDimensions(false) .withViewportMargin(this._viewportMargin) - .withScrollableContainers(scrollableAncestors); + .withScrollableContainers(scrollableAncestors) + .withPopoverLocation('global'); strategy.positionChanges.pipe(takeUntil(this._destroyed)).subscribe(change => { this._updateCurrentPositionClass(change.connectionPair); @@ -528,6 +529,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { panelClass: this._overlayPanelClass ? [...this._overlayPanelClass, panelClass] : panelClass, scrollStrategy: this._injector.get(MAT_TOOLTIP_SCROLL_STRATEGY)(), disableAnimations: this._animationsDisabled, + usePopover: true, }); this._updatePosition(this._overlayRef);