Skip to content

Commit 651549f

Browse files
crisbetovivian-hu-zz
authored andcommittedJan 16, 2019
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.
1 parent 0bd93dd commit 651549f

File tree

4 files changed

+97
-17
lines changed

4 files changed

+97
-17
lines changed
 

‎src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts

+47
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,53 @@ describe('FlexibleConnectedPositionStrategy', () => {
703703

704704
});
705705

706+
describe('with origin set to a point', () => {
707+
it('should be able to render at the primary position', () => {
708+
positionStrategy
709+
.setOrigin({x: 50, y: 100})
710+
.withPositions([{
711+
originX: 'start',
712+
originY: 'bottom',
713+
overlayX: 'start',
714+
overlayY: 'top'
715+
}]);
716+
717+
attachOverlay({positionStrategy});
718+
719+
const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
720+
expect(Math.floor(overlayRect.top)).toBe(100);
721+
expect(Math.floor(overlayRect.left)).toBe(50);
722+
});
723+
724+
it('should be able to render at a fallback position', () => {
725+
const viewportHeight = viewport.getViewportRect().height;
726+
727+
positionStrategy
728+
.setOrigin({x: 50, y: viewportHeight})
729+
.withPositions([
730+
{
731+
originX: 'start',
732+
originY: 'bottom',
733+
overlayX: 'start',
734+
overlayY: 'top'
735+
},
736+
{
737+
originX: 'start',
738+
originY: 'top',
739+
overlayX: 'start',
740+
overlayY: 'bottom'
741+
}
742+
]);
743+
744+
attachOverlay({positionStrategy});
745+
746+
const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
747+
expect(Math.floor(overlayRect.bottom)).toBe(viewportHeight);
748+
expect(Math.floor(overlayRect.left)).toBe(50);
749+
});
750+
751+
});
752+
706753
it('should account for the `offsetX` pushing the overlay out of the screen', () => {
707754
// Position the element so it would have enough space to fit.
708755
originElement.style.top = '200px';

‎src/cdk/overlay/position/flexible-connected-position-strategy.ts

+39-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import {Observable, Subscription, Subject, Observer} from 'rxjs';
2020
import {OverlayReference} from '../overlay-reference';
2121
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
22-
import {coerceCssPixelValue, coerceArray, coerceElement} from '@angular/cdk/coercion';
22+
import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion';
2323
import {Platform} from '@angular/cdk/platform';
2424
import {OverlayContainer} from '../overlay-container';
2525

@@ -29,6 +29,9 @@ import {OverlayContainer} from '../overlay-container';
2929
/** Class to be added to the overlay bounding box. */
3030
const boundingBoxClass = 'cdk-overlay-connected-position-bounding-box';
3131

32+
/** Possible values that can be set as the origin of a FlexibleConnectedPositionStrategy. */
33+
export type FlexibleConnectedPositionStrategyOrigin = ElementRef | HTMLElement | Point;
34+
3235
/**
3336
* A strategy for positioning overlays. Using this strategy, an overlay is given an
3437
* implicit position relative some origin element. The relative position is defined in terms of
@@ -80,7 +83,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
8083
_preferredPositions: ConnectionPositionPair[] = [];
8184

8285
/** The origin element against which the overlay will be positioned. */
83-
private _origin: HTMLElement;
86+
private _origin: FlexibleConnectedPositionStrategyOrigin;
8487

8588
/** The overlay pane element. */
8689
private _pane: HTMLElement;
@@ -139,7 +142,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
139142
}
140143

141144
constructor(
142-
connectedTo: ElementRef | HTMLElement,
145+
connectedTo: FlexibleConnectedPositionStrategyOrigin,
143146
private _viewportRuler: ViewportRuler,
144147
private _document: Document,
145148
// @breaking-change 8.0.0 `_platform` and `_overlayContainer` parameters to be made required.
@@ -211,7 +214,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
211214
// the overlay relative to the origin.
212215
// We use the viewport rect to determine whether a position would go off-screen.
213216
this._viewportRect = this._getNarrowedViewportRect();
214-
this._originRect = this._origin.getBoundingClientRect();
217+
this._originRect = this._getOriginRect();
215218
this._overlayRect = this._pane.getBoundingClientRect();
216219

217220
const originRect = this._originRect;
@@ -350,7 +353,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
350353
*/
351354
reapplyLastPosition(): void {
352355
if (!this._isDisposed && (!this._platform || this._platform.isBrowser)) {
353-
this._originRect = this._origin.getBoundingClientRect();
356+
this._originRect = this._getOriginRect();
354357
this._overlayRect = this._pane.getBoundingClientRect();
355358
this._viewportRect = this._getNarrowedViewportRect();
356359

@@ -427,11 +430,14 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
427430
}
428431

429432
/**
430-
* Sets the origin element, relative to which to position the overlay.
431-
* @param origin Reference to the new origin element.
433+
* Sets the origin, relative to which to position the overlay.
434+
* Using an element origin is useful for building components that need to be positioned
435+
* relatively to a trigger (e.g. dropdown menus or tooltips), whereas using a point can be
436+
* used for cases like contextual menus which open relative to the user's pointer.
437+
* @param origin Reference to the new origin.
432438
*/
433-
setOrigin(origin: ElementRef | HTMLElement): this {
434-
this._origin = coerceElement(origin);
439+
setOrigin(origin: FlexibleConnectedPositionStrategyOrigin): this {
440+
this._origin = origin;
435441
return this;
436442
}
437443

@@ -988,7 +994,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
988994
*/
989995
private _getScrollVisibility(): ScrollingVisibility {
990996
// Note: needs fresh rects since the position could've changed.
991-
const originBounds = this._origin.getBoundingClientRect();
997+
const originBounds = this._getOriginRect();
992998
const overlayBounds = this._pane.getBoundingClientRect();
993999

9941000
// TODO(jelbourn): instead of needing all of the client rects for these scrolling containers
@@ -1090,6 +1096,29 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
10901096
this._appliedPanelClasses = [];
10911097
}
10921098
}
1099+
1100+
/** Returns the ClientRect of the current origin. */
1101+
private _getOriginRect(): ClientRect {
1102+
const origin = this._origin;
1103+
1104+
if (origin instanceof ElementRef) {
1105+
return origin.nativeElement.getBoundingClientRect();
1106+
}
1107+
1108+
if (origin instanceof HTMLElement) {
1109+
return origin.getBoundingClientRect();
1110+
}
1111+
1112+
// If the origin is a point, return a client rect as if it was a 0x0 element at the point.
1113+
return {
1114+
top: origin.y,
1115+
bottom: origin.y,
1116+
left: origin.x,
1117+
right: origin.x,
1118+
height: 0,
1119+
width: 0
1120+
};
1121+
}
10931122
}
10941123

10951124
/** A simple (x, y) coordinate. */

‎src/cdk/overlay/position/overlay-position-builder.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {DOCUMENT} from '@angular/common';
1111
import {ElementRef, Inject, Injectable, Optional} from '@angular/core';
1212
import {OriginConnectionPosition, OverlayConnectionPosition} from './connected-position';
1313
import {ConnectedPositionStrategy} from './connected-position-strategy';
14-
import {FlexibleConnectedPositionStrategy} from './flexible-connected-position-strategy';
14+
import {
15+
FlexibleConnectedPositionStrategy,
16+
FlexibleConnectedPositionStrategyOrigin,
17+
} from './flexible-connected-position-strategy';
1518
import {GlobalPositionStrategy} from './global-position-strategy';
1619
import {Platform} from '@angular/cdk/platform';
1720
import {OverlayContainer} from '../overlay-container';
@@ -53,10 +56,11 @@ export class OverlayPositionBuilder {
5356

5457
/**
5558
* Creates a flexible position strategy.
56-
* @param elementRef
59+
* @param origin Origin relative to which to position the overlay.
5760
*/
58-
flexibleConnectedTo(elementRef: ElementRef | HTMLElement): FlexibleConnectedPositionStrategy {
59-
return new FlexibleConnectedPositionStrategy(elementRef, this._viewportRuler, this._document,
61+
flexibleConnectedTo(origin: FlexibleConnectedPositionStrategyOrigin):
62+
FlexibleConnectedPositionStrategy {
63+
return new FlexibleConnectedPositionStrategy(origin, this._viewportRuler, this._document,
6064
this._platform, this._overlayContainer);
6165
}
6266

‎tools/public_api_guard/cdk/overlay.d.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ export declare class FlexibleConnectedPositionStrategy implements PositionStrate
108108
_preferredPositions: ConnectionPositionPair[];
109109
positionChanges: Observable<ConnectedOverlayPositionChange>;
110110
readonly positions: ConnectionPositionPair[];
111-
constructor(connectedTo: ElementRef | HTMLElement, _viewportRuler: ViewportRuler, _document: Document, _platform?: Platform | undefined, _overlayContainer?: OverlayContainer | undefined);
111+
constructor(connectedTo: FlexibleConnectedPositionStrategyOrigin, _viewportRuler: ViewportRuler, _document: Document, _platform?: Platform | undefined, _overlayContainer?: OverlayContainer | undefined);
112112
apply(): void;
113113
attach(overlayRef: OverlayReference): void;
114114
detach(): void;
115115
dispose(): void;
116116
reapplyLastPosition(): void;
117-
setOrigin(origin: ElementRef | HTMLElement): this;
117+
setOrigin(origin: FlexibleConnectedPositionStrategyOrigin): this;
118118
withDefaultOffsetX(offset: number): this;
119119
withDefaultOffsetY(offset: number): this;
120120
withFlexibleDimensions(flexibleDimensions?: boolean): this;
@@ -216,7 +216,7 @@ export declare class OverlayModule {
216216
export declare class OverlayPositionBuilder {
217217
constructor(_viewportRuler: ViewportRuler, _document: any, _platform?: Platform | undefined, _overlayContainer?: OverlayContainer | undefined);
218218
connectedTo(elementRef: ElementRef, originPos: OriginConnectionPosition, overlayPos: OverlayConnectionPosition): ConnectedPositionStrategy;
219-
flexibleConnectedTo(elementRef: ElementRef | HTMLElement): FlexibleConnectedPositionStrategy;
219+
flexibleConnectedTo(origin: FlexibleConnectedPositionStrategyOrigin): FlexibleConnectedPositionStrategy;
220220
global(): GlobalPositionStrategy;
221221
}
222222

0 commit comments

Comments
 (0)
Please sign in to comment.