Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion goldens/cdk/overlay/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class BlockScrollStrategy implements ScrollStrategy {
// @public
export class CdkConnectedOverlay implements OnDestroy, OnChanges {
constructor(...args: unknown[]);
asPopover: boolean;
readonly attach: EventEmitter<void>;
attachOverlay(): void;
backdropClass: string | string[];
Expand All @@ -58,6 +59,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
minHeight: number | string;
minWidth: number | string;
// (undocumented)
static ngAcceptInputType_asPopover: unknown;
// (undocumented)
static ngAcceptInputType_disposeOnNavigation: unknown;
// (undocumented)
static ngAcceptInputType_flexibleDimensions: unknown;
Expand Down Expand Up @@ -92,7 +95,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
viewportMargin: ViewportMargin;
width: number | string;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkConnectedOverlay, "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", ["cdkConnectedOverlay"], { "origin": { "alias": "cdkConnectedOverlayOrigin"; "required": false; }; "positions": { "alias": "cdkConnectedOverlayPositions"; "required": false; }; "positionStrategy": { "alias": "cdkConnectedOverlayPositionStrategy"; "required": false; }; "offsetX": { "alias": "cdkConnectedOverlayOffsetX"; "required": false; }; "offsetY": { "alias": "cdkConnectedOverlayOffsetY"; "required": false; }; "width": { "alias": "cdkConnectedOverlayWidth"; "required": false; }; "height": { "alias": "cdkConnectedOverlayHeight"; "required": false; }; "minWidth": { "alias": "cdkConnectedOverlayMinWidth"; "required": false; }; "minHeight": { "alias": "cdkConnectedOverlayMinHeight"; "required": false; }; "backdropClass": { "alias": "cdkConnectedOverlayBackdropClass"; "required": false; }; "panelClass": { "alias": "cdkConnectedOverlayPanelClass"; "required": false; }; "viewportMargin": { "alias": "cdkConnectedOverlayViewportMargin"; "required": false; }; "scrollStrategy": { "alias": "cdkConnectedOverlayScrollStrategy"; "required": false; }; "open": { "alias": "cdkConnectedOverlayOpen"; "required": false; }; "disableClose": { "alias": "cdkConnectedOverlayDisableClose"; "required": false; }; "transformOriginSelector": { "alias": "cdkConnectedOverlayTransformOriginOn"; "required": false; }; "hasBackdrop": { "alias": "cdkConnectedOverlayHasBackdrop"; "required": false; }; "lockPosition": { "alias": "cdkConnectedOverlayLockPosition"; "required": false; }; "flexibleDimensions": { "alias": "cdkConnectedOverlayFlexibleDimensions"; "required": false; }; "growAfterOpen": { "alias": "cdkConnectedOverlayGrowAfterOpen"; "required": false; }; "push": { "alias": "cdkConnectedOverlayPush"; "required": false; }; "disposeOnNavigation": { "alias": "cdkConnectedOverlayDisposeOnNavigation"; "required": false; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkConnectedOverlay, "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", ["cdkConnectedOverlay"], { "origin": { "alias": "cdkConnectedOverlayOrigin"; "required": false; }; "positions": { "alias": "cdkConnectedOverlayPositions"; "required": false; }; "positionStrategy": { "alias": "cdkConnectedOverlayPositionStrategy"; "required": false; }; "offsetX": { "alias": "cdkConnectedOverlayOffsetX"; "required": false; }; "offsetY": { "alias": "cdkConnectedOverlayOffsetY"; "required": false; }; "width": { "alias": "cdkConnectedOverlayWidth"; "required": false; }; "height": { "alias": "cdkConnectedOverlayHeight"; "required": false; }; "minWidth": { "alias": "cdkConnectedOverlayMinWidth"; "required": false; }; "minHeight": { "alias": "cdkConnectedOverlayMinHeight"; "required": false; }; "backdropClass": { "alias": "cdkConnectedOverlayBackdropClass"; "required": false; }; "panelClass": { "alias": "cdkConnectedOverlayPanelClass"; "required": false; }; "viewportMargin": { "alias": "cdkConnectedOverlayViewportMargin"; "required": false; }; "scrollStrategy": { "alias": "cdkConnectedOverlayScrollStrategy"; "required": false; }; "open": { "alias": "cdkConnectedOverlayOpen"; "required": false; }; "disableClose": { "alias": "cdkConnectedOverlayDisableClose"; "required": false; }; "transformOriginSelector": { "alias": "cdkConnectedOverlayTransformOriginOn"; "required": false; }; "hasBackdrop": { "alias": "cdkConnectedOverlayHasBackdrop"; "required": false; }; "lockPosition": { "alias": "cdkConnectedOverlayLockPosition"; "required": false; }; "flexibleDimensions": { "alias": "cdkConnectedOverlayFlexibleDimensions"; "required": false; }; "growAfterOpen": { "alias": "cdkConnectedOverlayGrowAfterOpen"; "required": false; }; "push": { "alias": "cdkConnectedOverlayPush"; "required": false; }; "disposeOnNavigation": { "alias": "cdkConnectedOverlayDisposeOnNavigation"; "required": false; }; "asPopover": { "alias": "cdkConnectedOverlayAsPopover"; "required": false; }; }, { "backdropClick": "backdropClick"; "positionChange": "positionChange"; "attach": "attach"; "detach": "detach"; "overlayKeydown": "overlayKeydown"; "overlayOutsideClick": "overlayOutsideClick"; }, never, never, true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<CdkConnectedOverlay, never>;
}
Expand Down Expand Up @@ -222,7 +225,14 @@ export function createRepositionScrollStrategy(injector: Injector, config?: Repo
export class FlexibleConnectedPositionStrategy implements PositionStrategy {
constructor(connectedTo: FlexibleConnectedPositionStrategyOrigin, _viewportRuler: ViewportRuler, _document: Document, _platform: Platform, _overlayContainer: OverlayContainer);
apply(): void;
asPopover(isPopover: boolean): this;
attach(overlayRef: OverlayRef): void;
attachBackdrop(backdrop: HTMLElement, host: HTMLElement): boolean;
attachHost(host: HTMLElement): boolean;
createStructure(): {
pane: HTMLDivElement;
host: HTMLDivElement;
} | null;
// (undocumented)
detach(): void;
dispose(): void;
Expand All @@ -232,6 +242,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
_preferredPositions: ConnectionPositionPair[];
reapplyLastPosition(): void;
setOrigin(origin: FlexibleConnectedPositionStrategyOrigin): this;
updateStackingOrder(): boolean;
withDefaultOffsetX(offset: number): this;
withDefaultOffsetY(offset: number): this;
withFlexibleDimensions(flexibleDimensions?: boolean): this;
Expand Down Expand Up @@ -459,8 +470,15 @@ export interface OverlaySizeConfig {
export interface PositionStrategy {
apply(): void;
attach(overlayRef: OverlayRef): void;
attachBackdrop?(backdrop: HTMLElement, host: HTMLElement): boolean;
attachHost?(host: HTMLElement): boolean;
createStructure?(): {
pane: HTMLElement;
host: HTMLElement;
} | null;
detach?(): void;
dispose(): void;
updateStackingOrder?(host: HTMLElement): boolean;
}

// @public
Expand Down
23 changes: 23 additions & 0 deletions src/cdk/overlay/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,29 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
// block scrolling on a page that doesn't have a scrollbar in the first place.
overflow-y: scroll;
}

.cdk-overlay-popover {
background: none;
border: none;
padding: 0;
color: inherit;
outline: 0;
overflow: visible;
position: fixed;
pointer-events: none;

// These are important so the overlay can be measured before it's fully inserted.
width: 100%;
height: 100%;

// Chrome sets a user agent style of `inset: 0` which combined
// with `align-self` can break the positioning (see #29809).
inset: auto;

.cdk-overlay-backdrop {
position: fixed;
}
}
}

/// Emits structural styles required for cdk/overlay to function.
Expand Down
7 changes: 6 additions & 1 deletion src/cdk/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
this._disposeOnNavigation = value;
}

/** Whether the connected overlay should be rendered inside a popover element or the overlay container. */
@Input({alias: 'cdkConnectedOverlayAsPopover', transform: booleanAttribute})
asPopover: boolean = false;

/** Event emitted when the backdrop is clicked. */
@Output() readonly backdropClick = new EventEmitter<MouseEvent>();

Expand Down Expand Up @@ -376,7 +380,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
.withGrowAfterOpen(this.growAfterOpen)
.withViewportMargin(this.viewportMargin)
.withLockedPosition(this.lockPosition)
.withTransformOriginOn(this.transformOriginSelector);
.withTransformOriginOn(this.transformOriginSelector)
.asPopover(this.asPopover);
}

/** Returns the position strategy of the overlay to be set on the overlay config */
Expand Down
29 changes: 21 additions & 8 deletions src/cdk/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,23 @@ export class OverlayRef implements PortalOutlet {
attach(portal: Portal<any>): any {
// Insert the host into the DOM before attaching the portal, otherwise
// the animations module will skip animations on repeat attachments.
if (!this._host.parentElement && this._previousHostParent) {
this._previousHostParent.appendChild(this._host);
if (!this._host.parentElement) {
const hasAttachedHost = this._positionStrategy?.attachHost?.(this._host);

if (!hasAttachedHost && this._previousHostParent) {
this._previousHostParent.appendChild(this._host);
}
}

const attachResult = this._portalOutlet.attach(portal);
this._positionStrategy?.attach(this);

if (this._positionStrategy) {
this._positionStrategy.attach(this);
const hasUpdatedStackingOrder = this._positionStrategy?.updateStackingOrder?.(this._host);

if (!hasUpdatedStackingOrder) {
this._updateStackingOrder();
}

this._updateStackingOrder();
this._updateElementSize();
this._updateElementDirection();

Expand Down Expand Up @@ -426,9 +432,16 @@ export class OverlayRef implements PortalOutlet {
this._toggleClasses(this._backdropRef.element, this._config.backdropClass, true);
}

// Insert the backdrop before the pane in the DOM order,
// in order to handle stacked overlays properly.
this._host.parentElement!.insertBefore(this._backdropRef.element, this._host);
const strategyAttached = this._positionStrategy?.attachBackdrop?.(
this._backdropRef.element,
this._host,
);

if (!strategyAttached) {
// Insert the backdrop before the pane in the DOM order,
// in order to handle stacked overlays properly.
this._host.parentElement!.insertBefore(this._backdropRef.element, this._host);
}

// Add class to fade-in the backdrop after one frame.
if (!this._animationsDisabled && typeof requestAnimationFrame !== 'undefined') {
Expand Down
19 changes: 14 additions & 5 deletions src/cdk/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,26 @@ export function createOverlayRef(injector: Injector, config?: OverlayConfig): Ov
const idGenerator = injector.get(_IdGenerator);
const appRef = injector.get(ApplicationRef);
const directionality = injector.get(Directionality);
const overlayConfig = new OverlayConfig(config);
const customStructure = overlayConfig.positionStrategy?.createStructure?.();

let pane: HTMLElement;
let host: HTMLElement;

const host = doc.createElement('div');
const pane = doc.createElement('div');
if (customStructure) {
pane = customStructure.pane;
host = customStructure.host;
} else {
host = doc.createElement('div');
pane = doc.createElement('div');
host.appendChild(pane);
overlayContainer.getContainerElement().appendChild(host);
}

pane.id = idGenerator.getId('cdk-overlay-');
pane.classList.add('cdk-overlay-pane');
host.appendChild(pane);
overlayContainer.getContainerElement().appendChild(host);

const portalOutlet = new DomPortalOutlet(pane, appRef, injector);
const overlayConfig = new OverlayConfig(config);
const renderer =
injector.get(Renderer2, null, {optional: true}) ||
injector.get(RendererFactory2).createRenderer(null, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('FlexibleConnectedPositionStrategy', () => {
let overlayRef: OverlayRef;
let viewport: ViewportRuler;
let injector: Injector;
let portal: ComponentPortal<TestOverlay>;

beforeEach(() => {
injector = TestBed.inject(Injector);
Expand All @@ -50,7 +51,8 @@ describe('FlexibleConnectedPositionStrategy', () => {

function attachOverlay(config: OverlayConfig) {
overlayRef = createOverlayRef(injector, config);
overlayRef.attach(new ComponentPortal(TestOverlay));
portal = new ComponentPortal(TestOverlay);
overlayRef.attach(portal);
TestBed.inject(ApplicationRef).tick();
}

Expand Down Expand Up @@ -125,7 +127,7 @@ describe('FlexibleConnectedPositionStrategy', () => {
origin.remove();
});

it('should for the virtual keyboard offset when positioning the overlay', () => {
it('should account for the virtual keyboard offset when positioning the overlay', () => {
const originElement = createPositionedBlockElement();
document.body.appendChild(originElement);

Expand Down Expand Up @@ -2951,6 +2953,70 @@ describe('FlexibleConnectedPositionStrategy', () => {
expect(overlayClassList).toContain('custom-panel-class');
});
});

describe('DOM location', () => {
let positionStrategy: FlexibleConnectedPositionStrategy;
let containerElement: HTMLElement;
let originElement: HTMLElement;

beforeEach(() => {
containerElement = overlayContainer.getContainerElement();
originElement = createPositionedBlockElement();
document.body.appendChild(originElement);

positionStrategy = createFlexibleConnectedPositionStrategy(
injector,
originElement,
).withPositions([
{
overlayX: 'start',
overlayY: 'top',
originX: 'start',
originY: 'bottom',
},
]);
});

afterEach(() => {
originElement.remove();
});

it('should place the overlay inside the overlay container by default', () => {
attachOverlay({positionStrategy});
expect(containerElement.contains(overlayRef.hostElement)).toBe(true);
expect(overlayRef.hostElement.getAttribute('popover')).toBeFalsy();
});

it('should be able to opt into placing the overlay inside an adjacent popover element', () => {
if (!('showPopover' in document.body)) {
return;
}

positionStrategy.asPopover(true);
attachOverlay({positionStrategy});

expect(containerElement.contains(overlayRef.hostElement)).toBe(false);
expect(originElement.nextElementSibling).toBe(overlayRef.hostElement);
expect(overlayRef.hostElement.getAttribute('popover')).toBe('manual');
});

it('should re-attach the popover next to the origin element', () => {
if (!('showPopover' in document.body)) {
return;
}

positionStrategy.asPopover(true);
attachOverlay({positionStrategy});
expect(originElement.nextElementSibling).toBe(overlayRef.hostElement);

overlayRef.detach();
TestBed.inject(ApplicationRef).tick();
expect(overlayRef.hostElement.parentNode).toBeFalsy();

overlayRef.attach(portal);
expect(originElement.nextElementSibling).toBe(overlayRef.hostElement);
});
});
});

/** Creates an absolutely positioned, display: block element with a default size. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
/** Whether the overlay position is locked. */
private _positionLocked = false;

/** Whether the overlay is using popovers for positioning. */
private _popoverEnabled = false;

/** Cached origin dimensions */
private _originRect: Dimensions;

Expand Down Expand Up @@ -511,6 +514,73 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
return this;
}

/**
* Configures that the overlay should be rendered inside a native popover. This has the benefit
* if co-locating the overlay with the trigger and being better for accessibility.
* @param isPopover Whether the overlay should be a popover.
*/
asPopover(isPopover: boolean): this {
this._popoverEnabled = isPopover && 'showPopover' in this._document.body;
return this;
}

/** @docs-private */
createStructure() {
if (!this._popoverEnabled) {
return null;
}

const pane = this._document.createElement('div');
const host = this._document.createElement('div');
host.setAttribute('popover', 'manual');
host.classList.add('cdk-overlay-popover');
host.appendChild(pane);
this.attachHost(host);
return {pane, host};
}

/** @docs-private */
attachHost(host: HTMLElement): boolean {
if (!this._popoverEnabled) {
return false;
}

if (!host.parentNode) {
let originEl: Element | null;

if (this._origin instanceof ElementRef) {
originEl = this._origin.nativeElement;
} else if (typeof Element !== 'undefined' && this._origin instanceof Element) {
originEl = this._origin;
} else {
originEl = null;
}

if (originEl) {
originEl.after(host);
} else {
document.body.appendChild(host);
}
}

host.showPopover();
return true;
}

/** @docs-private */
attachBackdrop(backdrop: HTMLElement, host: HTMLElement): boolean {
if (this._popoverEnabled) {
host.prepend(backdrop);
}
return this._popoverEnabled;
}

/** @docs-private */
updateStackingOrder(): boolean {
// We don't need to update the stacking order since popovers handle it for us.
return this._popoverEnabled;
}

/**
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
*/
Expand Down
Loading
Loading