Skip to content

Commit 3dd191a

Browse files
committed
feat(overlay): allow for connected overlay to be positioned relative to a point
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 d22f48c commit 3dd191a

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,53 @@ describe('FlexibleConnectedPositionStrategy', () => {
700700

701701
});
702702

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

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

Lines changed: 39 additions & 10 deletions
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

@@ -987,7 +993,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
987993
*/
988994
private _getScrollVisibility(): ScrollingVisibility {
989995
// Note: needs fresh rects since the position could've changed.
990-
const originBounds = this._origin.getBoundingClientRect();
996+
const originBounds = this._getOriginRect();
991997
const overlayBounds = this._pane.getBoundingClientRect();
992998

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

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

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

Lines changed: 8 additions & 4 deletions
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

Lines changed: 3 additions & 3 deletions
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)