From 651549f7afa884fd717c0aa19fa904292f3da62e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 16 Jan 2019 19:40:58 +0100 Subject: [PATCH] feat(overlay): allow for connected overlay to be positioned relative to a point (#14616) Allows for the connected overlay's origin to be set to a point on the page, rather than a DOM element. This allows people to easily implement right click context menus. Relates to #5007. --- ...exible-connected-position-strategy.spec.ts | 47 ++++++++++++++++++ .../flexible-connected-position-strategy.ts | 49 +++++++++++++++---- .../position/overlay-position-builder.ts | 12 +++-- tools/public_api_guard/cdk/overlay.d.ts | 6 +-- 4 files changed, 97 insertions(+), 17 deletions(-) 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 a7e54b45529..2925191c49d 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -703,6 +703,53 @@ describe('FlexibleConnectedPositionStrategy', () => { }); + describe('with origin set to a point', () => { + it('should be able to render at the primary position', () => { + positionStrategy + .setOrigin({x: 50, y: 100}) + .withPositions([{ + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top' + }]); + + attachOverlay({positionStrategy}); + + const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + expect(Math.floor(overlayRect.top)).toBe(100); + expect(Math.floor(overlayRect.left)).toBe(50); + }); + + it('should be able to render at a fallback position', () => { + const viewportHeight = viewport.getViewportRect().height; + + positionStrategy + .setOrigin({x: 50, y: viewportHeight}) + .withPositions([ + { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top' + }, + { + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'bottom' + } + ]); + + attachOverlay({positionStrategy}); + + const overlayRect = overlayRef.overlayElement.getBoundingClientRect(); + expect(Math.floor(overlayRect.bottom)).toBe(viewportHeight); + expect(Math.floor(overlayRect.left)).toBe(50); + }); + + }); + it('should account for the `offsetX` pushing the overlay out of the screen', () => { // Position the element so it would have enough space to fit. originElement.style.top = '200px'; diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 7cac24a1262..ce83075ab35 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -19,7 +19,7 @@ import { import {Observable, Subscription, Subject, Observer} from 'rxjs'; import {OverlayReference} from '../overlay-reference'; import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip'; -import {coerceCssPixelValue, coerceArray, coerceElement} from '@angular/cdk/coercion'; +import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion'; import {Platform} from '@angular/cdk/platform'; import {OverlayContainer} from '../overlay-container'; @@ -29,6 +29,9 @@ import {OverlayContainer} from '../overlay-container'; /** Class to be added to the overlay bounding box. */ const boundingBoxClass = 'cdk-overlay-connected-position-bounding-box'; +/** Possible values that can be set as the origin of a FlexibleConnectedPositionStrategy. */ +export type FlexibleConnectedPositionStrategyOrigin = ElementRef | HTMLElement | Point; + /** * 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 @@ -80,7 +83,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { _preferredPositions: ConnectionPositionPair[] = []; /** The origin element against which the overlay will be positioned. */ - private _origin: HTMLElement; + private _origin: FlexibleConnectedPositionStrategyOrigin; /** The overlay pane element. */ private _pane: HTMLElement; @@ -139,7 +142,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } constructor( - connectedTo: ElementRef | HTMLElement, + connectedTo: FlexibleConnectedPositionStrategyOrigin, private _viewportRuler: ViewportRuler, private _document: Document, // @breaking-change 8.0.0 `_platform` and `_overlayContainer` parameters to be made required. @@ -211,7 +214,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // the overlay relative to the origin. // We use the viewport rect to determine whether a position would go off-screen. this._viewportRect = this._getNarrowedViewportRect(); - this._originRect = this._origin.getBoundingClientRect(); + this._originRect = this._getOriginRect(); this._overlayRect = this._pane.getBoundingClientRect(); const originRect = this._originRect; @@ -350,7 +353,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { */ reapplyLastPosition(): void { if (!this._isDisposed && (!this._platform || this._platform.isBrowser)) { - this._originRect = this._origin.getBoundingClientRect(); + this._originRect = this._getOriginRect(); this._overlayRect = this._pane.getBoundingClientRect(); this._viewportRect = this._getNarrowedViewportRect(); @@ -427,11 +430,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } /** - * Sets the origin element, relative to which to position the overlay. - * @param origin Reference to the new origin element. + * Sets the origin, relative to which to position the overlay. + * Using an element origin is useful for building components that need to be positioned + * relatively to a trigger (e.g. dropdown menus or tooltips), whereas using a point can be + * used for cases like contextual menus which open relative to the user's pointer. + * @param origin Reference to the new origin. */ - setOrigin(origin: ElementRef | HTMLElement): this { - this._origin = coerceElement(origin); + setOrigin(origin: FlexibleConnectedPositionStrategyOrigin): this { + this._origin = origin; return this; } @@ -988,7 +994,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { */ private _getScrollVisibility(): ScrollingVisibility { // Note: needs fresh rects since the position could've changed. - const originBounds = this._origin.getBoundingClientRect(); + const originBounds = this._getOriginRect(); const overlayBounds = this._pane.getBoundingClientRect(); // TODO(jelbourn): instead of needing all of the client rects for these scrolling containers @@ -1090,6 +1096,29 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { this._appliedPanelClasses = []; } } + + /** Returns the ClientRect of the current origin. */ + private _getOriginRect(): ClientRect { + const origin = this._origin; + + if (origin instanceof ElementRef) { + return origin.nativeElement.getBoundingClientRect(); + } + + if (origin instanceof HTMLElement) { + return origin.getBoundingClientRect(); + } + + // If the origin is a point, return a client rect as if it was a 0x0 element at the point. + return { + top: origin.y, + bottom: origin.y, + left: origin.x, + right: origin.x, + height: 0, + width: 0 + }; + } } /** A simple (x, y) coordinate. */ diff --git a/src/cdk/overlay/position/overlay-position-builder.ts b/src/cdk/overlay/position/overlay-position-builder.ts index fa8ef74b46f..fbaed18021e 100644 --- a/src/cdk/overlay/position/overlay-position-builder.ts +++ b/src/cdk/overlay/position/overlay-position-builder.ts @@ -11,7 +11,10 @@ import {DOCUMENT} from '@angular/common'; import {ElementRef, Inject, Injectable, Optional} from '@angular/core'; import {OriginConnectionPosition, OverlayConnectionPosition} from './connected-position'; import {ConnectedPositionStrategy} from './connected-position-strategy'; -import {FlexibleConnectedPositionStrategy} from './flexible-connected-position-strategy'; +import { + FlexibleConnectedPositionStrategy, + FlexibleConnectedPositionStrategyOrigin, +} from './flexible-connected-position-strategy'; import {GlobalPositionStrategy} from './global-position-strategy'; import {Platform} from '@angular/cdk/platform'; import {OverlayContainer} from '../overlay-container'; @@ -53,10 +56,11 @@ export class OverlayPositionBuilder { /** * Creates a flexible position strategy. - * @param elementRef + * @param origin Origin relative to which to position the overlay. */ - flexibleConnectedTo(elementRef: ElementRef | HTMLElement): FlexibleConnectedPositionStrategy { - return new FlexibleConnectedPositionStrategy(elementRef, this._viewportRuler, this._document, + flexibleConnectedTo(origin: FlexibleConnectedPositionStrategyOrigin): + FlexibleConnectedPositionStrategy { + return new FlexibleConnectedPositionStrategy(origin, this._viewportRuler, this._document, this._platform, this._overlayContainer); } diff --git a/tools/public_api_guard/cdk/overlay.d.ts b/tools/public_api_guard/cdk/overlay.d.ts index 81c60a7a26a..b759a9484ac 100644 --- a/tools/public_api_guard/cdk/overlay.d.ts +++ b/tools/public_api_guard/cdk/overlay.d.ts @@ -108,13 +108,13 @@ export declare class FlexibleConnectedPositionStrategy implements PositionStrate _preferredPositions: ConnectionPositionPair[]; positionChanges: Observable; readonly positions: ConnectionPositionPair[]; - constructor(connectedTo: ElementRef | HTMLElement, _viewportRuler: ViewportRuler, _document: Document, _platform?: Platform | undefined, _overlayContainer?: OverlayContainer | undefined); + constructor(connectedTo: FlexibleConnectedPositionStrategyOrigin, _viewportRuler: ViewportRuler, _document: Document, _platform?: Platform | undefined, _overlayContainer?: OverlayContainer | undefined); apply(): void; attach(overlayRef: OverlayReference): void; detach(): void; dispose(): void; reapplyLastPosition(): void; - setOrigin(origin: ElementRef | HTMLElement): this; + setOrigin(origin: FlexibleConnectedPositionStrategyOrigin): this; withDefaultOffsetX(offset: number): this; withDefaultOffsetY(offset: number): this; withFlexibleDimensions(flexibleDimensions?: boolean): this; @@ -216,7 +216,7 @@ export declare class OverlayModule { export declare class OverlayPositionBuilder { constructor(_viewportRuler: ViewportRuler, _document: any, _platform?: Platform | undefined, _overlayContainer?: OverlayContainer | undefined); connectedTo(elementRef: ElementRef, originPos: OriginConnectionPosition, overlayPos: OverlayConnectionPosition): ConnectedPositionStrategy; - flexibleConnectedTo(elementRef: ElementRef | HTMLElement): FlexibleConnectedPositionStrategy; + flexibleConnectedTo(origin: FlexibleConnectedPositionStrategyOrigin): FlexibleConnectedPositionStrategy; global(): GlobalPositionStrategy; }