diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index f3d7a5f9ff45..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) @@ -267,7 +265,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; @@ -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[]); @@ -327,7 +329,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 +337,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 +525,7 @@ export interface PositionStrategy { attach(overlayRef: OverlayRef): void; detach?(): void; dispose(): void; - getPopoverInsertionPoint?(): Element; + getPopoverInsertionPoint?(): Element | null; } // @public 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%; 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/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/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/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; } 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, 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);