Skip to content

feat(overlay): allow for connected overlay to be positioned relative to a point #14616

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 16, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,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';
Expand Down
49 changes: 39 additions & 10 deletions src/cdk/overlay/position/flexible-connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -987,7 +993,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
Expand Down Expand Up @@ -1089,6 +1095,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. */
Expand Down
12 changes: 8 additions & 4 deletions src/cdk/overlay/position/overlay-position-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}

Expand Down
6 changes: 3 additions & 3 deletions tools/public_api_guard/cdk/overlay.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ export declare class FlexibleConnectedPositionStrategy implements PositionStrate
_preferredPositions: ConnectionPositionPair[];
positionChanges: Observable<ConnectedOverlayPositionChange>;
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;
Expand Down Expand Up @@ -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;
}

Expand Down